Skip to content

Commit e434373

Browse files
authored
feat(core): support for style direction property (ltr/rtl) (#10691)
1 parent af00e73 commit e434373

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+676
-173
lines changed

apps/automated/src/application/application-tests.android.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ export function testAndroidApplicationInitialized() {
4343
TKUnit.assert(
4444
// @ts-expect-error
4545
Application.android.foregroundActivity.isNativeScriptActivity,
46-
'Android foregroundActivity.isNativeScriptActivity is false.'
46+
'Android foregroundActivity.isNativeScriptActivity is false.',
4747
);
4848
TKUnit.assert(Application.android.startActivity, 'Android startActivity not initialized.');
4949
TKUnit.assert(Application.android.nativeApp, 'Android nativeApp not initialized.');
5050
TKUnit.assert(Application.android.orientation(), 'Android orientation not initialized.');
5151
TKUnit.assert(Utils.android.getPackageName(), 'Android packageName not initialized.');
5252
TKUnit.assert(Application.android.systemAppearance(), 'Android system appearance not initialized.');
53+
TKUnit.assert(Application.android.layoutDirection(), 'Android layout direction not initialized.');
5354
}
5455

5556
export function testSystemAppearance() {

apps/automated/src/application/application-tests.ios.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function testIOSApplicationInitialized() {
5252
TKUnit.assert(Application.ios.systemAppearance(), 'iOS system appearance not initialized.');
5353
}
5454

55+
TKUnit.assert(Application.ios.layoutDirection(), 'iOS layout direction not initialized.');
5556
TKUnit.assert(Application.ios.window, 'iOS window not initialized.');
5657
TKUnit.assert(Application.ios.rootController, 'iOS root controller not initialized.');
5758
}

apps/automated/src/ui/label/label-tests-native.android.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,29 @@ import * as labelModule from '@nativescript/core/ui/label';
22
import { Color, CoreTypes } from '@nativescript/core';
33
import { AndroidHelper } from '@nativescript/core/ui/core/view';
44

5+
const UNEXPECTED_VALUE = 'unexpected value';
6+
57
export function getNativeTextAlignment(label: labelModule.Label): string {
6-
let gravity = label.android.getGravity();
8+
let hGravity = label.android.getGravity() & android.view.Gravity.HORIZONTAL_GRAVITY_MASK;
9+
const alignment = label.android.getTextAlignment();
10+
11+
if (hGravity === android.view.Gravity.START && alignment === android.view.View.TEXT_ALIGNMENT_VIEW_START) {
12+
return 'initial';
13+
}
714

8-
if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.LEFT) {
15+
if (hGravity === android.view.Gravity.LEFT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) {
916
return CoreTypes.TextAlignment.left;
1017
}
1118

12-
if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.CENTER_HORIZONTAL) {
19+
if (hGravity === android.view.Gravity.CENTER_HORIZONTAL && alignment === android.view.View.TEXT_ALIGNMENT_CENTER) {
1320
return CoreTypes.TextAlignment.center;
1421
}
1522

16-
if ((gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK) === android.view.Gravity.RIGHT) {
23+
if (hGravity === android.view.Gravity.RIGHT && alignment === android.view.View.TEXT_ALIGNMENT_GRAVITY) {
1724
return CoreTypes.TextAlignment.right;
1825
}
1926

20-
return 'unexpected value';
27+
return UNEXPECTED_VALUE;
2128
}
2229

2330
export function getNativeBackgroundColor(label: labelModule.Label): Color {

apps/automated/src/ui/label/label-tests-native.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ import * as labelModule from '@nativescript/core/ui/label';
33
import * as colorModule from '@nativescript/core/color';
44

55
export declare function getNativeTextAlignment(label: labelModule.Label): string;
6-
76
export declare function getNativeBackgroundColor(label: labelModule.Label): colorModule.Color;

apps/automated/src/ui/label/label-tests.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,8 @@ export class LabelTest extends testModule.UITest<Label> {
305305

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

312312
actualColors = testLabel.android.getTextColors();

apps/automated/src/ui/styling/root-views-css-classes-tests.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const LANDSCAPE_ORIENTATION_CSS_CLASS = 'ns-landscape';
1717
const UNKNOWN_ORIENTATION_CSS_CLASS = 'ns-unknown';
1818
const DARK_SYSTEM_APPEARANCE_CSS_CLASS = 'ns-dark';
1919
const LIGHT_SYSTEM_APPEARANCE_CSS_CLASS = 'ns-light';
20+
const LTR_LAYOUT_DIRECTION_CSS_CLASS = 'ns-ltr';
21+
const RTL_LAYOUT_DIRECTION_CSS_CLASS = 'ns-rtl';
2022

2123
function _test_root_css_class(view: View, isModal: boolean, shouldSetClassName: boolean) {
2224
if (shouldSetClassName) {
@@ -78,12 +80,14 @@ function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean
7880
}
7981

8082
const cssClasses = rootView.cssClasses;
81-
let appOrientation;
83+
84+
let appOrientation: 'portrait' | 'landscape' | 'unknown';
8285
if (isAndroid) {
83-
appOrientation = Application.android.orientation;
86+
appOrientation = Application.android.orientation();
8487
} else {
85-
appOrientation = Application.ios.orientation;
88+
appOrientation = Application.ios.orientation();
8689
}
90+
8791
if (appOrientation === 'portrait') {
8892
TKUnit.assertTrue(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is missing`);
8993
TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`);
@@ -92,7 +96,7 @@ function _test_orientation_css_class(rootView: View, shouldSetClassName: boolean
9296
TKUnit.assertTrue(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is missing`);
9397
TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`);
9498
TKUnit.assertFalse(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is present`);
95-
} else if (appOrientation === 'landscape') {
99+
} else {
96100
TKUnit.assertTrue(cssClasses.has(UNKNOWN_ORIENTATION_CSS_CLASS), `${UNKNOWN_ORIENTATION_CSS_CLASS} CSS class is missing`);
97101
TKUnit.assertFalse(cssClasses.has(LANDSCAPE_ORIENTATION_CSS_CLASS), `${LANDSCAPE_ORIENTATION_CSS_CLASS} CSS class is present`);
98102
TKUnit.assertFalse(cssClasses.has(PORTRAIT_ORIENTATION_CSS_CLASS), `${PORTRAIT_ORIENTATION_CSS_CLASS} CSS class is present`);
@@ -109,12 +113,14 @@ function _test_system_appearance_css_class(rootView: View, shouldSetClassName: b
109113
}
110114

111115
const cssClasses = rootView.cssClasses;
112-
let systemAppearance;
116+
117+
let systemAppearance: 'dark' | 'light' | null;
113118
if (isAndroid) {
114-
systemAppearance = Application.android.systemAppearance;
119+
systemAppearance = Application.android.systemAppearance();
115120
} else {
116-
systemAppearance = Application.ios.systemAppearance;
121+
systemAppearance = Application.ios.systemAppearance();
117122
}
123+
118124
if (isIOS && !__VISIONOS__ && Utils.SDK_VERSION <= 12) {
119125
TKUnit.assertFalse(cssClasses.has(DARK_SYSTEM_APPEARANCE_CSS_CLASS), `${DARK_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`);
120126
TKUnit.assertFalse(cssClasses.has(LIGHT_SYSTEM_APPEARANCE_CSS_CLASS), `${LIGHT_SYSTEM_APPEARANCE_CSS_CLASS} CSS class is present`);
@@ -131,6 +137,36 @@ function _test_system_appearance_css_class(rootView: View, shouldSetClassName: b
131137
}
132138
}
133139

140+
function _test_layout_direction_css_class(rootView: View, shouldSetClassName: boolean) {
141+
if (shouldSetClassName) {
142+
rootView.className = CLASS_NAME;
143+
}
144+
145+
const cssClasses = rootView.cssClasses;
146+
147+
let appLayoutDirection: CoreTypes.LayoutDirectionType | null;
148+
if (isAndroid) {
149+
appLayoutDirection = Application.android.layoutDirection();
150+
} else {
151+
appLayoutDirection = Application.ios.layoutDirection();
152+
}
153+
154+
if (appLayoutDirection === 'ltr') {
155+
TKUnit.assertTrue(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is missing`);
156+
TKUnit.assertFalse(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
157+
} else if (appLayoutDirection === 'rtl') {
158+
TKUnit.assertTrue(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is missing`);
159+
TKUnit.assertFalse(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
160+
} else {
161+
TKUnit.assertFalse(cssClasses.has(LTR_LAYOUT_DIRECTION_CSS_CLASS), `${LTR_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
162+
TKUnit.assertFalse(cssClasses.has(RTL_LAYOUT_DIRECTION_CSS_CLASS), `${RTL_LAYOUT_DIRECTION_CSS_CLASS} CSS class is present`);
163+
}
164+
165+
if (shouldSetClassName) {
166+
TKUnit.assertTrue(cssClasses.has(CLASS_NAME), `${CLASS_NAME} CSS class is missing`);
167+
}
168+
}
169+
134170
// Application root view
135171
export function test_root_view_root_css_class() {
136172
const rootView = Application.getRootView();

packages/core/application/application-common.ts

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { Frame } from '../ui/frame';
1111
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
1212
import type { StyleScope } from '../ui/styling/style-scope';
1313
import type { AndroidApplication as AndroidApplicationType, iOSApplication as iOSApplicationType } from '.';
14-
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, UnhandledErrorEventData } from './application-interfaces';
14+
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, LayoutDirectionChangedEventData, UnhandledErrorEventData } from './application-interfaces';
1515
import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common';
1616
import { getAppMainEntry, isAppInBackground, setAppInBackground, setAppMainEntry } from './helpers-common';
1717
import { getNativeScriptGlobals } from '../globals/global-utils';
@@ -30,6 +30,12 @@ const SYSTEM_APPEARANCE_CSS_CLASSES = [
3030
`${CSSUtils.CLASS_PREFIX}${CoreTypes.SystemAppearance.dark}`,
3131
];
3232

33+
// prettier-ignore
34+
const LAYOUT_DIRECTION_CSS_CLASSES = [
35+
`${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.ltr}`,
36+
`${CSSUtils.CLASS_PREFIX}${CoreTypes.LayoutDirection.rtl}`,
37+
];
38+
3339
// SDK Version CSS classes
3440
let sdkVersionClasses: string[] = [];
3541

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

173+
/**
174+
* This event is raised when the operating system layout direction changes
175+
* between ltr and rtl.
176+
*/
177+
on(event: 'layoutDirectionChanged', callback: (args: LayoutDirectionChangedEventData) => void, thisArg?: any): void;
178+
167179
on(event: 'fontScaleChanged', callback: (args: FontScaleChangedEventData) => void, thisArg?: any): void;
168180
}
169181

@@ -180,6 +192,7 @@ export class ApplicationCommon {
180192
readonly discardedErrorEvent = 'discardedError';
181193
readonly orientationChangedEvent = 'orientationChanged';
182194
readonly systemAppearanceChangedEvent = 'systemAppearanceChanged';
195+
readonly layoutDirectionChangedEvent = 'layoutDirectionChanged';
183196
readonly fontScaleChangedEvent = 'fontScaleChanged';
184197
readonly livesyncEvent = 'livesync';
185198
readonly loadAppCssEvent = 'loadAppCss';
@@ -215,6 +228,21 @@ export class ApplicationCommon {
215228
notify: ApplicationEvents['notify'] = globalEvents.notify.bind(globalEvents);
216229
hasListeners: ApplicationEvents['hasListeners'] = globalEvents.hasListeners.bind(globalEvents);
217230

231+
private _orientation: 'portrait' | 'landscape' | 'unknown';
232+
private _systemAppearance: 'dark' | 'light' | null;
233+
private _layoutDirection: CoreTypes.LayoutDirectionType | null;
234+
private _inBackground: boolean = false;
235+
private _suspended: boolean = false;
236+
private _cssFile = './app.css';
237+
238+
protected mainEntry: NavigationEntry;
239+
240+
public started = false;
241+
/**
242+
* Boolean to enable/disable systemAppearanceChanged
243+
*/
244+
public autoSystemAppearanceChanged = true;
245+
218246
/**
219247
* @internal - should not be constructed by the user.
220248
*/
@@ -321,6 +349,7 @@ export class ApplicationCommon {
321349
const deviceType = Device.deviceType.toLowerCase();
322350
const orientation = this.orientation();
323351
const systemAppearance = this.systemAppearance();
352+
const layoutDirection = this.layoutDirection();
324353

325354
if (platform) {
326355
CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${platform}`);
@@ -338,6 +367,10 @@ export class ApplicationCommon {
338367
CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${systemAppearance}`);
339368
}
340369

370+
if (layoutDirection) {
371+
CSSUtils.pushToSystemCssClasses(`${CSSUtils.CLASS_PREFIX}${layoutDirection}`);
372+
}
373+
341374
rootView.cssClasses.add(CSSUtils.ROOT_VIEW_CSS_CLASS);
342375
const rootViewCssClasses = CSSUtils.getSystemCssClasses();
343376
rootViewCssClasses.forEach((c) => rootView.cssClasses.add(c));
@@ -447,12 +480,11 @@ export class ApplicationCommon {
447480
bindableResources.set(res);
448481
}
449482

450-
private cssFile = './app.css';
451483
/**
452484
* Sets css file name for the application.
453485
*/
454486
setCssFileName(cssFileName: string) {
455-
this.cssFile = cssFileName;
487+
this._cssFile = cssFileName;
456488
this.notify(<CssChangedEventData>{
457489
eventName: this.cssChangedEvent,
458490
object: this,
@@ -464,7 +496,7 @@ export class ApplicationCommon {
464496
* Gets css file name for the application.
465497
*/
466498
getCssFileName(): string {
467-
return this.cssFile;
499+
return this._cssFile;
468500
}
469501

470502
/**
@@ -507,8 +539,6 @@ export class ApplicationCommon {
507539
throw new Error('run() Not implemented.');
508540
}
509541

510-
private _orientation: 'portrait' | 'landscape' | 'unknown';
511-
512542
protected getOrientation(): 'portrait' | 'landscape' | 'unknown' {
513543
// override in platform specific Application class
514544
throw new Error('getOrientation() not implemented');
@@ -568,8 +598,6 @@ export class ApplicationCommon {
568598
return getNativeScriptGlobals().launched;
569599
}
570600

571-
private _systemAppearance: 'dark' | 'light' | null;
572-
573601
protected getSystemAppearance(): 'dark' | 'light' | null {
574602
// override in platform specific Application class
575603
throw new Error('getSystemAppearance() not implemented');
@@ -595,11 +623,6 @@ export class ApplicationCommon {
595623
return (this._systemAppearance ??= this.getSystemAppearance());
596624
}
597625

598-
/**
599-
* Boolean to enable/disable systemAppearanceChanged
600-
*/
601-
autoSystemAppearanceChanged = true;
602-
603626
/**
604627
* enable/disable systemAppearanceChanged
605628
*/
@@ -632,6 +655,56 @@ export class ApplicationCommon {
632655
rootView._onCssStateChange();
633656
}
634657

658+
protected getLayoutDirection(): CoreTypes.LayoutDirectionType | null {
659+
// override in platform specific Application class
660+
throw new Error('getLayoutDirection() not implemented');
661+
}
662+
663+
protected setLayoutDirection(value: CoreTypes.LayoutDirectionType) {
664+
if (this._layoutDirection === value) {
665+
return;
666+
}
667+
this._layoutDirection = value;
668+
this.layoutDirectionChanged(this.getRootView(), value);
669+
this.notify(<LayoutDirectionChangedEventData>{
670+
eventName: this.layoutDirectionChangedEvent,
671+
android: this.android,
672+
ios: this.ios,
673+
newValue: value,
674+
object: this,
675+
});
676+
}
677+
678+
layoutDirection(): CoreTypes.LayoutDirectionType | null {
679+
// return cached value, or get it from the platform specific override
680+
return (this._layoutDirection ??= this.getLayoutDirection());
681+
}
682+
683+
/**
684+
* Updates root view classes including those of modals
685+
* @param rootView the root view
686+
* @param newLayoutDirection the new layout direction change
687+
*/
688+
layoutDirectionChanged(rootView: View, newLayoutDirection: CoreTypes.LayoutDirectionType): void {
689+
if (!rootView) {
690+
return;
691+
}
692+
693+
const newLayoutDirectionCssClass = `${CSSUtils.CLASS_PREFIX}${newLayoutDirection}`;
694+
this.applyCssClass(rootView, LAYOUT_DIRECTION_CSS_CLASSES, newLayoutDirectionCssClass, true);
695+
696+
const rootModalViews = rootView._getRootModalViews();
697+
rootModalViews.forEach((rootModalView) => {
698+
this.applyCssClass(rootModalView as View, LAYOUT_DIRECTION_CSS_CLASSES, newLayoutDirectionCssClass, true);
699+
700+
// Trigger state change for root modal view classes and media queries
701+
rootModalView._onCssStateChange();
702+
});
703+
704+
// Trigger state change for root view classes and media queries
705+
rootView._onCssStateChange();
706+
}
707+
635708
get inBackground() {
636709
return isAppInBackground();
637710
}
@@ -648,8 +721,6 @@ export class ApplicationCommon {
648721
});
649722
}
650723

651-
private _suspended: boolean = false;
652-
653724
get suspended() {
654725
return this._suspended;
655726
}
@@ -667,8 +738,6 @@ export class ApplicationCommon {
667738
});
668739
}
669740

670-
public started = false;
671-
672741
get android(): AndroidApplicationType {
673742
return undefined;
674743
}

0 commit comments

Comments
 (0)