Skip to content

feat: iOS 26 types with improvements (ActionBar, Switch) + .ns-{platform}-{sdkVersion} css root scoping #10775

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

Merged
merged 8 commits into from
Aug 7, 2025
Merged
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
9 changes: 9 additions & 0 deletions apps/toolbox/src/_app-platform.ios.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,12 @@
.list-group {
margin-top: 7;
}

.ns-ios-26 .action-bar {
/**
* Note: nativescript-theme-core auto adds background-color to actionbar
* doing so will cause iOS 26+ to not render titles when large title is enabled
* We can now scope specific styling behavior for platform sdk versions
*/
background-color: transparent;
}
4 changes: 2 additions & 2 deletions apps/toolbox/src/pages/a11y.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
</ActionBar>
</Page.actionBar>

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

Expand Down
23 changes: 12 additions & 11 deletions apps/toolbox/src/pages/image-async.xml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">

<GridLayout padding="20">
<ScrollView>
<StackLayout>
<Button text="Image Async?" tap="{{save}}" />
<Image width="200" height="200" src="{{ src }}"/>
<Image marginTop="10" width="200" height="200" src="{{ savedData }}" />
<Image marginTop="10" width="50" height="50" imageSource="{{ resizedImage }}" />
</StackLayout>
</ScrollView>
</GridLayout>
<Page.actionBar>
<ActionBar title="Image Async" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<ScrollView>
<StackLayout padding="20">
<Button text="Image Async?" tap="{{save}}" />
<Image width="200" height="200" src="{{ src }}"/>
<Image marginTop="10" width="200" height="200" src="{{ savedData }}" />
<Image marginTop="10" width="50" height="50" imageSource="{{ resizedImage }}" />
</StackLayout>
</ScrollView>
</Page>
55 changes: 55 additions & 0 deletions packages/core/accessibility/accessibility-css-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Application } from '../application';
import type { View } from '../ui/core/view';
import { AccessibilityServiceEnabledObservable } from './accessibility-service';
import { FontScaleCategory, getCurrentFontScale, getFontScaleCategory, VALID_FONT_SCALES } from './font-scale';
import { SDK_VERSION } from '../utils/constants';

// CSS-classes
const fontScaleExtraSmallCategoryClass = `a11y-fontscale-xs`;
Expand All @@ -14,6 +15,9 @@ const a11yServiceEnabledClass = `a11y-service-enabled`;
const a11yServiceDisabledClass = `a11y-service-disabled`;
const a11yServiceClasses = [a11yServiceEnabledClass, a11yServiceDisabledClass];

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

let accessibilityServiceObservable: AccessibilityServiceEnabledObservable;
let fontScaleCssClasses: Map<number, string>;

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

accessibilityServiceObservable = new AccessibilityServiceEnabledObservable();

// Initialize SDK version CSS class once
initializeSdkVersionClass();
}

function initializeSdkVersionClass(): void {
const majorVersion = Math.floor(SDK_VERSION);
sdkVersionClasses = [];

let platformPrefix = '';
if (__APPLE__) {
platformPrefix = __VISIONOS__ ? 'ns-visionos' : 'ns-ios';
} else if (__ANDROID__) {
platformPrefix = 'ns-android';
}

if (platformPrefix) {
// Add exact version class (e.g., .ns-ios-26 or .ns-android-36)
// this acts like 'gte' for that major version range
// e.g., if user wants iOS 27, they can add .ns-ios-27 specifiers
sdkVersionClasses.push(`${platformPrefix}-${majorVersion}`);
}

// Apply the SDK version classes to root views
applySdkVersionClass();
}

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

const rootModalViews = <Array<View>>rootView._getRootModalViews();
rootModalViews.forEach((rootModalView) => Application.applyCssClass(rootModalView, cssClasses, newCssClass));

// Note: SDK version classes are applied separately to avoid redundant work
}

function applySdkVersionClass(): void {
if (!sdkVersionClasses.length) {
return;
}

const rootView = Application.getRootView();
if (!rootView) {
return;
}

// Batch apply all SDK version classes to root view for better performance
const classesToAdd = sdkVersionClasses.filter((className) => !rootView.cssClasses.has(className));
classesToAdd.forEach((className) => rootView.cssClasses.add(className));

// Apply to modal views only if there are any
const rootModalViews = <Array<View>>rootView._getRootModalViews();
if (rootModalViews.length > 0) {
rootModalViews.forEach((rootModalView) => {
const modalClassesToAdd = sdkVersionClasses.filter((className) => !rootModalView.cssClasses.has(className));
modalClassesToAdd.forEach((className) => rootModalView.cssClasses.add(className));
});
}
}

function applyFontScaleToRootViews(): void {
Expand Down
48 changes: 37 additions & 11 deletions packages/core/ui/action-bar/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import { LinearGradient } from '../styling/linear-gradient';
import { colorProperty, backgroundInternalProperty, backgroundColorProperty, backgroundImageProperty } from '../styling/style-properties';
import { ios as iosViewUtils } from '../utils';
import { ImageSource } from '../../image-source';
import { layout, iOSNativeHelper, isFontIconURI } from '../../utils';
import { layout, isFontIconURI } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties';

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

const majorVersion = iOSNativeHelper.MajorVersion;
const UNSPECIFIED = layout.makeMeasureSpec(0, layout.UNSPECIFIED);

interface NSUINavigationBar extends UINavigationBar {
Expand Down Expand Up @@ -271,7 +270,7 @@ export class ActionBar extends ActionBarBase {
// show the one from the old page but the new page will still be visible (because we canceled EdgeBackSwipe gesutre)
// Consider moving this to new method and call it from - navigationControllerDidShowViewControllerAnimated.
const image = img ? img.imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) : null;
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
const appearance = this._getAppearance(navigationBar);
appearance.setBackIndicatorImageTransitionMaskImage(image, image);
this._updateAppearance(navigationBar, appearance);
Expand Down Expand Up @@ -304,6 +303,9 @@ export class ActionBar extends ActionBarBase {
navigationItem.accessibilityLabel = this.accessibilityLabel;
navigationItem.accessibilityLanguage = this.accessibilityLanguage;
navigationItem.accessibilityHint = this.accessibilityHint;

// Configure large title support for this navigation item
this.checkLargeTitleSupport(navigationItem);
}

private populateMenuItems(navigationItem: UINavigationItem) {
Expand Down Expand Up @@ -378,7 +380,7 @@ export class ActionBar extends ActionBarBase {
}
if (color) {
const titleTextColor = NSDictionary.dictionaryWithObjectForKey(color.ios, NSForegroundColorAttributeName);
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
appearance.titleTextAttributes = titleTextColor;
}
Expand All @@ -398,7 +400,7 @@ export class ActionBar extends ActionBarBase {
}

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

let color: UIColor;

if (__VISIONOS__ || majorVersion >= 15) {
if (__VISIONOS__ || SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
color = appearance.backgroundColor;
} else {
Expand All @@ -432,7 +434,7 @@ export class ActionBar extends ActionBarBase {
return;
}

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

let image: UIImage;

if (__VISIONOS__ || majorVersion >= 15) {
if (__VISIONOS__ || SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
image = appearance.backgroundImage;
} else {
Expand Down Expand Up @@ -507,6 +509,8 @@ export class ActionBar extends ActionBarBase {
return;
}

console.log('ActionBar._onTitlePropertyChanged', this.title);

if (page.frame) {
page.frame._updateActionBar(page);
}
Expand All @@ -517,7 +521,7 @@ export class ActionBar extends ActionBarBase {

private updateFlatness(navBar: UINavigationBar) {
if (this.flat) {
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
const appearance = this._getAppearance(navBar);
appearance.shadowColor = UIColor.clearColor;
this._updateAppearance(navBar, appearance);
Expand All @@ -530,7 +534,7 @@ export class ActionBar extends ActionBarBase {
navBar.translucent = false;
}
} else {
if (majorVersion >= 15) {
if (SDK_VERSION >= 15) {
if (navBar.standardAppearance) {
// Not flat and never been set do nothing.
const appearance = navBar.standardAppearance;
Expand Down Expand Up @@ -581,7 +585,7 @@ export class ActionBar extends ActionBarBase {
public onLayout(left: number, top: number, right: number, bottom: number) {
const titleView = this.titleView;
if (titleView) {
if (majorVersion > 10) {
if (SDK_VERSION > 10) {
// On iOS 11 titleView is wrapped in another view that is centered with constraints.
View.layoutChild(this, titleView, 0, 0, titleView.getMeasuredWidth(), titleView.getMeasuredHeight());
} else {
Expand Down Expand Up @@ -670,4 +674,26 @@ export class ActionBar extends ActionBarBase {
this.navBar.prefersLargeTitles = value;
}
}

private checkLargeTitleSupport(navigationItem: UINavigationItem) {
const navBar = this.navBar;
if (!navBar) {
return;
}
// Configure large title display mode only when not using a custom titleView
if (SDK_VERSION >= 11) {
if (this.iosLargeTitle) {
// Always show large title for this navigation item when large titles are enabled
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Always;
} else {
if (SDK_VERSION >= 26) {
// Explicitly disable large titles for this navigation item
// Due to overlapping title issue in iOS 26
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Never;
} else {
navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Automatic;
}
}
}
}
}
36 changes: 18 additions & 18 deletions packages/core/ui/frame/frame-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@ export enum NavigationType {
replace,
}

export interface TransitionState {
enterTransitionListener: any;
exitTransitionListener: any;
reenterTransitionListener: any;
returnTransitionListener: any;
transitionName: string;
entry: BackstackEntry;
}

export interface ViewEntry {
moduleName?: string;
create?: () => View;
Expand All @@ -35,6 +26,17 @@ export interface NavigationEntry extends ViewEntry {
clearHistory?: boolean;
}

export interface BackstackEntry {
entry: NavigationEntry;
resolvedPage: Page;
navDepth: number;
fragmentTag: string;
fragment?: any;
viewSavedState?: any;
frameId?: number;
recreated?: boolean;
}

export interface NavigationContext {
entry?: BackstackEntry;
/**
Expand All @@ -51,15 +53,13 @@ export interface NavigationTransition {
curve?: any;
}

export interface BackstackEntry {
entry: NavigationEntry;
resolvedPage: Page;
navDepth: number;
fragmentTag: string;
fragment?: any;
viewSavedState?: any;
frameId?: number;
recreated?: boolean;
export interface TransitionState {
enterTransitionListener: any;
exitTransitionListener: any;
reenterTransitionListener: any;
returnTransitionListener: any;
transitionName: string;
entry: BackstackEntry;
}

export interface AndroidFrame extends Observable {
Expand Down
1 change: 1 addition & 0 deletions packages/core/ui/frame/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NavigatedData, Page } from '../page';
import { Observable, EventData } from '../../data/observable';
import { Property, View } from '../core/view';
import { Transition } from '../transition';
import { BackstackEntry } from './frame-interfaces';

export * from './frame-interfaces';

Expand Down
3 changes: 2 additions & 1 deletion packages/core/ui/frame/index.ios.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//Types
import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from '.';
import { iOSFrame as iOSFrameDefinition, NavigationTransition } from '.';
import type { BackstackEntry } from './frame-interfaces';
import { FrameBase, NavigationType } from './frame-common';
import { Page } from '../page';
import { View } from '../core/view';
Expand Down
Loading