Skip to content

Commit 9914454

Browse files
authored
feat: iOS 26 types with improvements (ActionBar, Switch) + .ns-{platform}-{sdkVersion} css root scoping (NativeScript#10775)
This provides for better ability to target platform > sdk > majorVersion specific features. For example, iOS 26 does not render titles when a background color is set on the actionbar. this allows that style to be overridden only on iOS 26 if desired.
1 parent adc2ee4 commit 9914454

File tree

144 files changed

+51051
-38677
lines changed

Some content is hidden

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

144 files changed

+51051
-38677
lines changed

apps/toolbox/src/_app-platform.ios.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,12 @@
9191
.list-group {
9292
margin-top: 7;
9393
}
94+
95+
.ns-ios-26 .action-bar {
96+
/**
97+
* Note: nativescript-theme-core auto adds background-color to actionbar
98+
* doing so will cause iOS 26+ to not render titles when large title is enabled
99+
* We can now scope specific styling behavior for platform sdk versions
100+
*/
101+
background-color: transparent;
102+
}

apps/toolbox/src/pages/a11y.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
</ActionBar>
55
</Page.actionBar>
66

7-
<GridLayout padding="20" class="a11y-demo-page">
7+
<GridLayout class="a11y-demo-page">
88
<ScrollView>
9-
<StackLayout>
9+
<StackLayout padding="20">
1010
<Button testID="openModalPageButton" text="Open Modal Page" class="view-item" tap="{{openModal}}" />
1111
<Button testID="openNormalPageButton" text="Open Normal Page" class="view-item" tap="{{openNormal}}" />
1212

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
2-
3-
<GridLayout padding="20">
4-
<ScrollView>
5-
<StackLayout>
6-
<Button text="Image Async?" tap="{{save}}" />
7-
<Image width="200" height="200" src="{{ src }}"/>
8-
<Image marginTop="10" width="200" height="200" src="{{ savedData }}" />
9-
<Image marginTop="10" width="50" height="50" imageSource="{{ resizedImage }}" />
10-
</StackLayout>
11-
</ScrollView>
12-
</GridLayout>
2+
<Page.actionBar>
3+
<ActionBar title="Image Async" icon="" class="action-bar">
4+
</ActionBar>
5+
</Page.actionBar>
6+
<ScrollView>
7+
<StackLayout padding="20">
8+
<Button text="Image Async?" tap="{{save}}" />
9+
<Image width="200" height="200" src="{{ src }}"/>
10+
<Image marginTop="10" width="200" height="200" src="{{ savedData }}" />
11+
<Image marginTop="10" width="50" height="50" imageSource="{{ resizedImage }}" />
12+
</StackLayout>
13+
</ScrollView>
1314
</Page>

packages/core/accessibility/accessibility-css-helper.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Application } from '../application';
22
import type { View } from '../ui/core/view';
33
import { AccessibilityServiceEnabledObservable } from './accessibility-service';
44
import { FontScaleCategory, getCurrentFontScale, getFontScaleCategory, VALID_FONT_SCALES } from './font-scale';
5+
import { SDK_VERSION } from '../utils/constants';
56

67
// CSS-classes
78
const fontScaleExtraSmallCategoryClass = `a11y-fontscale-xs`;
@@ -14,6 +15,9 @@ const a11yServiceEnabledClass = `a11y-service-enabled`;
1415
const a11yServiceDisabledClass = `a11y-service-disabled`;
1516
const a11yServiceClasses = [a11yServiceEnabledClass, a11yServiceDisabledClass];
1617

18+
// SDK Version CSS classes
19+
let sdkVersionClasses: string[] = [];
20+
1721
let accessibilityServiceObservable: AccessibilityServiceEnabledObservable;
1822
let fontScaleCssClasses: Map<number, string>;
1923

@@ -29,6 +33,31 @@ function ensureClasses() {
2933
fontScaleCssClasses = new Map(VALID_FONT_SCALES.map((fs) => [fs, `a11y-fontscale-${Number(fs * 100).toFixed(0)}`]));
3034

3135
accessibilityServiceObservable = new AccessibilityServiceEnabledObservable();
36+
37+
// Initialize SDK version CSS class once
38+
initializeSdkVersionClass();
39+
}
40+
41+
function initializeSdkVersionClass(): void {
42+
const majorVersion = Math.floor(SDK_VERSION);
43+
sdkVersionClasses = [];
44+
45+
let platformPrefix = '';
46+
if (__APPLE__) {
47+
platformPrefix = __VISIONOS__ ? 'ns-visionos' : 'ns-ios';
48+
} else if (__ANDROID__) {
49+
platformPrefix = 'ns-android';
50+
}
51+
52+
if (platformPrefix) {
53+
// Add exact version class (e.g., .ns-ios-26 or .ns-android-36)
54+
// this acts like 'gte' for that major version range
55+
// e.g., if user wants iOS 27, they can add .ns-ios-27 specifiers
56+
sdkVersionClasses.push(`${platformPrefix}-${majorVersion}`);
57+
}
58+
59+
// Apply the SDK version classes to root views
60+
applySdkVersionClass();
3261
}
3362

3463
function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
@@ -41,6 +70,32 @@ function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
4170

4271
const rootModalViews = <Array<View>>rootView._getRootModalViews();
4372
rootModalViews.forEach((rootModalView) => Application.applyCssClass(rootModalView, cssClasses, newCssClass));
73+
74+
// Note: SDK version classes are applied separately to avoid redundant work
75+
}
76+
77+
function applySdkVersionClass(): void {
78+
if (!sdkVersionClasses.length) {
79+
return;
80+
}
81+
82+
const rootView = Application.getRootView();
83+
if (!rootView) {
84+
return;
85+
}
86+
87+
// Batch apply all SDK version classes to root view for better performance
88+
const classesToAdd = sdkVersionClasses.filter((className) => !rootView.cssClasses.has(className));
89+
classesToAdd.forEach((className) => rootView.cssClasses.add(className));
90+
91+
// Apply to modal views only if there are any
92+
const rootModalViews = <Array<View>>rootView._getRootModalViews();
93+
if (rootModalViews.length > 0) {
94+
rootModalViews.forEach((rootModalView) => {
95+
const modalClassesToAdd = sdkVersionClasses.filter((className) => !rootModalView.cssClasses.has(className));
96+
modalClassesToAdd.forEach((className) => rootModalView.cssClasses.add(className));
97+
});
98+
}
4499
}
45100

46101
function applyFontScaleToRootViews(): void {

packages/core/ui/action-bar/index.ios.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ import { LinearGradient } from '../styling/linear-gradient';
77
import { colorProperty, backgroundInternalProperty, backgroundColorProperty, backgroundImageProperty } from '../styling/style-properties';
88
import { ios as iosViewUtils } from '../utils';
99
import { ImageSource } from '../../image-source';
10-
import { layout, iOSNativeHelper, isFontIconURI } from '../../utils';
10+
import { layout, isFontIconURI } from '../../utils';
1111
import { SDK_VERSION } from '../../utils/constants';
1212
import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties';
1313

1414
export * from './action-bar-common';
1515

16-
const majorVersion = iOSNativeHelper.MajorVersion;
1716
const UNSPECIFIED = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
1817

1918
interface NSUINavigationBar extends UINavigationBar {
@@ -271,7 +270,7 @@ export class ActionBar extends ActionBarBase {
271270
// show the one from the old page but the new page will still be visible (because we canceled EdgeBackSwipe gesutre)
272271
// Consider moving this to new method and call it from - navigationControllerDidShowViewControllerAnimated.
273272
const image = img ? img.imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) : null;
274-
if (majorVersion >= 15) {
273+
if (SDK_VERSION >= 15) {
275274
const appearance = this._getAppearance(navigationBar);
276275
appearance.setBackIndicatorImageTransitionMaskImage(image, image);
277276
this._updateAppearance(navigationBar, appearance);
@@ -304,6 +303,9 @@ export class ActionBar extends ActionBarBase {
304303
navigationItem.accessibilityLabel = this.accessibilityLabel;
305304
navigationItem.accessibilityLanguage = this.accessibilityLanguage;
306305
navigationItem.accessibilityHint = this.accessibilityHint;
306+
307+
// Configure large title support for this navigation item
308+
this.checkLargeTitleSupport(navigationItem);
307309
}
308310

309311
private populateMenuItems(navigationItem: UINavigationItem) {
@@ -378,7 +380,7 @@ export class ActionBar extends ActionBarBase {
378380
}
379381
if (color) {
380382
const titleTextColor = NSDictionary.dictionaryWithObjectForKey(color.ios, NSForegroundColorAttributeName);
381-
if (majorVersion >= 15) {
383+
if (SDK_VERSION >= 15) {
382384
const appearance = this._getAppearance(navBar);
383385
appearance.titleTextAttributes = titleTextColor;
384386
}
@@ -398,7 +400,7 @@ export class ActionBar extends ActionBarBase {
398400
}
399401

400402
const nativeColor = color instanceof Color ? color.ios : color;
401-
if (__VISIONOS__ || majorVersion >= 15) {
403+
if (__VISIONOS__ || SDK_VERSION >= 15) {
402404
const appearance = this._getAppearance(navBar);
403405
// appearance.configureWithOpaqueBackground();
404406
appearance.backgroundColor = nativeColor;
@@ -416,7 +418,7 @@ export class ActionBar extends ActionBarBase {
416418

417419
let color: UIColor;
418420

419-
if (__VISIONOS__ || majorVersion >= 15) {
421+
if (__VISIONOS__ || SDK_VERSION >= 15) {
420422
const appearance = this._getAppearance(navBar);
421423
color = appearance.backgroundColor;
422424
} else {
@@ -432,7 +434,7 @@ export class ActionBar extends ActionBarBase {
432434
return;
433435
}
434436

435-
if (__VISIONOS__ || majorVersion >= 15) {
437+
if (__VISIONOS__ || SDK_VERSION >= 15) {
436438
const appearance = this._getAppearance(navBar);
437439
// appearance.configureWithOpaqueBackground();
438440
appearance.backgroundImage = image;
@@ -456,7 +458,7 @@ export class ActionBar extends ActionBarBase {
456458

457459
let image: UIImage;
458460

459-
if (__VISIONOS__ || majorVersion >= 15) {
461+
if (__VISIONOS__ || SDK_VERSION >= 15) {
460462
const appearance = this._getAppearance(navBar);
461463
image = appearance.backgroundImage;
462464
} else {
@@ -507,6 +509,8 @@ export class ActionBar extends ActionBarBase {
507509
return;
508510
}
509511

512+
console.log('ActionBar._onTitlePropertyChanged', this.title);
513+
510514
if (page.frame) {
511515
page.frame._updateActionBar(page);
512516
}
@@ -517,7 +521,7 @@ export class ActionBar extends ActionBarBase {
517521

518522
private updateFlatness(navBar: UINavigationBar) {
519523
if (this.flat) {
520-
if (majorVersion >= 15) {
524+
if (SDK_VERSION >= 15) {
521525
const appearance = this._getAppearance(navBar);
522526
appearance.shadowColor = UIColor.clearColor;
523527
this._updateAppearance(navBar, appearance);
@@ -530,7 +534,7 @@ export class ActionBar extends ActionBarBase {
530534
navBar.translucent = false;
531535
}
532536
} else {
533-
if (majorVersion >= 15) {
537+
if (SDK_VERSION >= 15) {
534538
if (navBar.standardAppearance) {
535539
// Not flat and never been set do nothing.
536540
const appearance = navBar.standardAppearance;
@@ -581,7 +585,7 @@ export class ActionBar extends ActionBarBase {
581585
public onLayout(left: number, top: number, right: number, bottom: number) {
582586
const titleView = this.titleView;
583587
if (titleView) {
584-
if (majorVersion > 10) {
588+
if (SDK_VERSION > 10) {
585589
// On iOS 11 titleView is wrapped in another view that is centered with constraints.
586590
View.layoutChild(this, titleView, 0, 0, titleView.getMeasuredWidth(), titleView.getMeasuredHeight());
587591
} else {
@@ -670,4 +674,26 @@ export class ActionBar extends ActionBarBase {
670674
this.navBar.prefersLargeTitles = value;
671675
}
672676
}
677+
678+
private checkLargeTitleSupport(navigationItem: UINavigationItem) {
679+
const navBar = this.navBar;
680+
if (!navBar) {
681+
return;
682+
}
683+
// Configure large title display mode only when not using a custom titleView
684+
if (SDK_VERSION >= 11) {
685+
if (this.iosLargeTitle) {
686+
// Always show large title for this navigation item when large titles are enabled
687+
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Always;
688+
} else {
689+
if (SDK_VERSION >= 26) {
690+
// Explicitly disable large titles for this navigation item
691+
// Due to overlapping title issue in iOS 26
692+
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Never;
693+
} else {
694+
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Automatic;
695+
}
696+
}
697+
}
698+
}
673699
}

packages/core/ui/frame/frame-interfaces.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@ export enum NavigationType {
1010
replace,
1111
}
1212

13-
export interface TransitionState {
14-
enterTransitionListener: any;
15-
exitTransitionListener: any;
16-
reenterTransitionListener: any;
17-
returnTransitionListener: any;
18-
transitionName: string;
19-
entry: BackstackEntry;
20-
}
21-
2213
export interface ViewEntry {
2314
moduleName?: string;
2415
create?: () => View;
@@ -35,6 +26,17 @@ export interface NavigationEntry extends ViewEntry {
3526
clearHistory?: boolean;
3627
}
3728

29+
export interface BackstackEntry {
30+
entry: NavigationEntry;
31+
resolvedPage: Page;
32+
navDepth: number;
33+
fragmentTag: string;
34+
fragment?: any;
35+
viewSavedState?: any;
36+
frameId?: number;
37+
recreated?: boolean;
38+
}
39+
3840
export interface NavigationContext {
3941
entry?: BackstackEntry;
4042
/**
@@ -51,15 +53,13 @@ export interface NavigationTransition {
5153
curve?: any;
5254
}
5355

54-
export interface BackstackEntry {
55-
entry: NavigationEntry;
56-
resolvedPage: Page;
57-
navDepth: number;
58-
fragmentTag: string;
59-
fragment?: any;
60-
viewSavedState?: any;
61-
frameId?: number;
62-
recreated?: boolean;
56+
export interface TransitionState {
57+
enterTransitionListener: any;
58+
exitTransitionListener: any;
59+
reenterTransitionListener: any;
60+
returnTransitionListener: any;
61+
transitionName: string;
62+
entry: BackstackEntry;
6363
}
6464

6565
export interface AndroidFrame extends Observable {

packages/core/ui/frame/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NavigatedData, Page } from '../page';
33
import { Observable, EventData } from '../../data/observable';
44
import { Property, View } from '../core/view';
55
import { Transition } from '../transition';
6+
import { BackstackEntry } from './frame-interfaces';
67

78
export * from './frame-interfaces';
89

packages/core/ui/frame/index.ios.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//Types
2-
import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from '.';
2+
import { iOSFrame as iOSFrameDefinition, NavigationTransition } from '.';
3+
import type { BackstackEntry } from './frame-interfaces';
34
import { FrameBase, NavigationType } from './frame-common';
45
import { Page } from '../page';
56
import { View } from '../core/view';

0 commit comments

Comments
 (0)