Files
NativeScript/packages/core/accessibility/accessibility-css-helper.ts
Nathan Walker 991445461b feat: iOS 26 types with improvements (ActionBar, Switch) + .ns-{platform}-{sdkVersion} css root scoping (#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.
2025-08-07 12:06:10 -07:00

181 lines
5.6 KiB
TypeScript

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`;
const fontScaleMediumCategoryClass = `a11y-fontscale-m`;
const fontScaleExtraLargeCategoryClass = `a11y-fontscale-xl`;
const fontScaleCategoryClasses = [fontScaleExtraSmallCategoryClass, fontScaleMediumCategoryClass, fontScaleExtraLargeCategoryClass];
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>;
let currentFontScaleClass = '';
let currentFontScaleCategory = '';
let currentA11YServiceClass = '';
function ensureClasses() {
if (accessibilityServiceObservable) {
return;
}
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 {
const rootView = Application.getRootView();
if (!rootView) {
return;
}
Application.applyCssClass(rootView, cssClasses, newCssClass);
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 {
const rootView = Application.getRootView();
if (!rootView) {
return;
}
const fontScale = getCurrentFontScale();
rootView.style.fontScaleInternal = fontScale;
const rootModalViews = <Array<View>>rootView._getRootModalViews();
rootModalViews.forEach((rootModalView) => (rootModalView.style.fontScaleInternal = fontScale));
}
export function initAccessibilityCssHelper(): void {
ensureClasses();
Application.on(Application.fontScaleChangedEvent, () => {
updateCurrentHelperClasses();
applyFontScaleToRootViews();
});
accessibilityServiceObservable.on(AccessibilityServiceEnabledObservable.propertyChangeEvent, updateCurrentHelperClasses);
}
/**
* Update the helper CSS-classes.
* Return true is any changes.
*/
function updateCurrentHelperClasses(): void {
const fontScale = getCurrentFontScale();
const fontScaleCategory = getFontScaleCategory();
const oldFontScaleClass = currentFontScaleClass;
if (fontScaleCssClasses.has(fontScale)) {
currentFontScaleClass = fontScaleCssClasses.get(fontScale);
} else {
currentFontScaleClass = fontScaleCssClasses.get(1);
}
if (oldFontScaleClass !== currentFontScaleClass) {
applyRootCssClass([...fontScaleCssClasses.values()], currentFontScaleClass);
}
const oldActiveFontScaleCategory = currentFontScaleCategory;
switch (fontScaleCategory) {
case FontScaleCategory.ExtraSmall: {
currentFontScaleCategory = fontScaleExtraSmallCategoryClass;
break;
}
case FontScaleCategory.Medium: {
currentFontScaleCategory = fontScaleMediumCategoryClass;
break;
}
case FontScaleCategory.ExtraLarge: {
currentFontScaleCategory = fontScaleExtraLargeCategoryClass;
break;
}
default: {
currentFontScaleCategory = fontScaleMediumCategoryClass;
break;
}
}
if (oldActiveFontScaleCategory !== currentFontScaleCategory) {
applyRootCssClass(fontScaleCategoryClasses, currentFontScaleCategory);
}
const oldA11YStatusClass = currentA11YServiceClass;
if (accessibilityServiceObservable.accessibilityServiceEnabled) {
currentA11YServiceClass = a11yServiceEnabledClass;
} else {
currentA11YServiceClass = a11yServiceDisabledClass;
}
if (oldA11YStatusClass !== currentA11YServiceClass) {
applyRootCssClass(a11yServiceClasses, currentA11YServiceClass);
}
}