From 7aaa1d899dc786ccd6e9e73a4bd94674ddeba12e Mon Sep 17 00:00:00 2001 From: Dimitris-Rafail Katsampas Date: Thu, 6 Apr 2023 02:20:15 +0300 Subject: [PATCH] feat(ios): new a11y properties for managing font scale (#10260) --- .../accessibility-properties-tests.ts | 70 +++++++++++++++++++ apps/automated/src/test-runner.ts | 3 + .../src/ui/styling/style-properties-tests.ts | 12 ++-- .../accessibility/accessibility-css-helper.ts | 4 +- .../accessibility/accessibility-properties.ts | 24 +++++++ packages/core/ui/core/view/index.d.ts | 15 ++++ packages/core/ui/core/view/view-common.ts | 23 +++++- .../core/ui/styling/style-properties.d.ts | 9 +-- packages/core/ui/styling/style-properties.ts | 14 ++-- packages/core/ui/styling/style/index.ts | 5 +- packages/core/ui/text-base/index.ios.ts | 51 ++++++++++---- 11 files changed, 196 insertions(+), 34 deletions(-) create mode 100644 apps/automated/src/accessibility/accessibility-properties-tests.ts diff --git a/apps/automated/src/accessibility/accessibility-properties-tests.ts b/apps/automated/src/accessibility/accessibility-properties-tests.ts new file mode 100644 index 000000000..6c40f6120 --- /dev/null +++ b/apps/automated/src/accessibility/accessibility-properties-tests.ts @@ -0,0 +1,70 @@ +import * as TKUnit from '../tk-unit'; +import * as helper from '../ui-helper'; +import { isIOS, Label, StackLayout } from '@nativescript/core'; + +export function test_iosAccessibilityAdjustsFontSize_property() { + if (isIOS) { + const deviceFontScaleMock = 4.0; + + const page = helper.getCurrentPage(); + const testView = new Label(); + const layout = new StackLayout(); + layout.addChild(testView); + + page.content = layout; + + layout.style.iosAccessibilityAdjustsFontSize = false; + layout.style.fontScaleInternal = deviceFontScaleMock; + + const nativeFontSize = testView.nativeTextViewProtected.font.pointSize; + layout.style.iosAccessibilityAdjustsFontSize = true; + const nativeFontSizeWithAdjust = testView.nativeTextViewProtected.font.pointSize; + + TKUnit.assertEqual(nativeFontSize, testView.style.fontInternal.fontSize, 'View font size was scaled even though iosAccessibilityAdjustsFontSize is disabled'); + TKUnit.assertEqual(nativeFontSizeWithAdjust, testView.style.fontInternal.fontSize * deviceFontScaleMock, 'View font size was not scaled even though iosAccessibilityAdjustsFontSize is enabled'); + } +} + +export function test_iosAccessibilityMinFontScale_property() { + if (isIOS) { + const deviceFontScaleMock = 1.0; + + const page = helper.getCurrentPage(); + const testView = new Label(); + const layout = new StackLayout(); + layout.addChild(testView); + + page.content = layout; + + layout.style.iosAccessibilityAdjustsFontSize = true; + layout.style.fontScaleInternal = deviceFontScaleMock; + + testView.style.iosAccessibilityMinFontScale = 2.0; + + const nativeFontSize = testView.nativeTextViewProtected.font.pointSize; + const expectedNativeFontSize = testView.style.fontInternal.fontSize * testView.style.iosAccessibilityMinFontScale; + TKUnit.assertEqual(nativeFontSize, expectedNativeFontSize, 'View font size scaling does not respect iosAccessibilityMinFontScale'); + } +} + +export function test_iosAccessibilityMaxFontScale_property() { + if (isIOS) { + const deviceFontScaleMock = 4.0; + + const page = helper.getCurrentPage(); + const testView = new Label(); + const layout = new StackLayout(); + layout.addChild(testView); + + page.content = layout; + + layout.style.iosAccessibilityAdjustsFontSize = true; + layout.style.fontScaleInternal = deviceFontScaleMock; + + testView.style.iosAccessibilityMaxFontScale = 2.0; + + const nativeFontSize = testView.nativeTextViewProtected.font.pointSize; + const expectedNativeFontSize = testView.style.fontInternal.fontSize * testView.style.iosAccessibilityMaxFontScale; + TKUnit.assertEqual(nativeFontSize, expectedNativeFontSize, 'View font size scaling does not respect iosAccessibilityMaxFontScale'); + } +} diff --git a/apps/automated/src/test-runner.ts b/apps/automated/src/test-runner.ts index 2283b1898..b7571723d 100644 --- a/apps/automated/src/test-runner.ts +++ b/apps/automated/src/test-runner.ts @@ -54,6 +54,9 @@ if (!__CI__) { allTests['PROFILING'] = profilingTests; } +import * as a11yPropertiesTests from './accessibility/accessibility-properties-tests'; +allTests['A11Y-PROPERTIES'] = a11yPropertiesTests; + import * as appSettingsTests from './application-settings/application-settings-tests'; allTests['APPLICATION-SETTINGS'] = appSettingsTests; diff --git a/apps/automated/src/ui/styling/style-properties-tests.ts b/apps/automated/src/ui/styling/style-properties-tests.ts index 8870592e5..d878bb2a2 100644 --- a/apps/automated/src/ui/styling/style-properties-tests.ts +++ b/apps/automated/src/ui/styling/style-properties-tests.ts @@ -582,13 +582,17 @@ export function test_setting_font_properties_sets_native_font() { export function test_native_font_size_with_a11y_font_scale() { if (isIOS) { - const page = helper.getCurrentPage(); - const testView = new Label(); const deviceFontScaleMock = 4.0; - page.content = testView; + const page = helper.getCurrentPage(); + const testView = new Label(); + const layout = new StackLayout(); + layout.addChild(testView); - testView.style._fontScale = deviceFontScaleMock; + page.content = layout; + + layout.style.iosAccessibilityAdjustsFontSize = true; + layout.style.fontScaleInternal = deviceFontScaleMock; const nativeFontSize = testView.nativeTextViewProtected.font.pointSize; const expectedNativeFontSize = testView.style.fontInternal.fontSize * deviceFontScaleMock; diff --git a/packages/core/accessibility/accessibility-css-helper.ts b/packages/core/accessibility/accessibility-css-helper.ts index e029a4fc6..78e162d06 100644 --- a/packages/core/accessibility/accessibility-css-helper.ts +++ b/packages/core/accessibility/accessibility-css-helper.ts @@ -52,10 +52,10 @@ function applyFontScaleToRootViews(): void { const fontScale = getCurrentFontScale(); - rootView.style._fontScale = fontScale; + rootView.style.fontScaleInternal = fontScale; const rootModalViews = >rootView._getRootModalViews(); - rootModalViews.forEach((rootModalView) => (rootModalView.style._fontScale = fontScale)); + rootModalViews.forEach((rootModalView) => (rootModalView.style.fontScaleInternal = fontScale)); } export function initAccessibilityCssHelper(): void { diff --git a/packages/core/accessibility/accessibility-properties.ts b/packages/core/accessibility/accessibility-properties.ts index 9d899e15c..3e10c6cf2 100644 --- a/packages/core/accessibility/accessibility-properties.ts +++ b/packages/core/accessibility/accessibility-properties.ts @@ -31,6 +31,30 @@ export const accessibilityEnabledProperty = new CssProperty({ }); accessibilityEnabledProperty.register(Style); +export const iosAccessibilityAdjustsFontSizeProperty = new InheritedCssProperty({ + defaultValue: false, + name: 'iosAccessibilityAdjustsFontSize', + cssName: 'ios-a11y-adjusts-font-size', + valueConverter: booleanConverter, +}); +iosAccessibilityAdjustsFontSizeProperty.register(Style); + +export const iosAccessibilityMinFontScaleProperty = new InheritedCssProperty({ + defaultValue: 0, + name: 'iosAccessibilityMinFontScale', + cssName: 'ios-a11y-min-font-scale', + valueConverter: parseFloat, +}); +iosAccessibilityMinFontScaleProperty.register(Style); + +export const iosAccessibilityMaxFontScaleProperty = new InheritedCssProperty({ + defaultValue: 0, + name: 'iosAccessibilityMaxFontScale', + cssName: 'ios-a11y-max-font-scale', + valueConverter: parseFloat, +}); +iosAccessibilityMaxFontScaleProperty.register(Style); + export const accessibilityHiddenProperty = new (global.isIOS ? InheritedCssProperty : CssProperty)({ name: 'accessibilityHidden', cssName: 'a11y-hidden', diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 7b4778ddd..583249209 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -282,6 +282,21 @@ export abstract class View extends ViewCommon { */ accessibilityMediaSession: boolean; + /** + * Defines whether accessibility font scale should affect font size. + */ + iosAccessibilityAdjustsFontSize: boolean; + + /** + * Gets or sets the minimum accessibility font scale. + */ + iosAccessibilityMinFontScale: number; + + /** + * Gets or sets the maximum accessibility font scale. + */ + iosAccessibilityMaxFontScale: number; + /** * Internal use only. This is used to limit the number of updates to android.view.View.setContentDescription() */ diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 82846b14c..c3e626a84 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -405,7 +405,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { modalRootViewCssClasses.forEach((c) => this.cssClasses.add(c)); parent._modal = this; - this.style._fontScale = getCurrentFontScale(); + this.style.fontScaleInternal = getCurrentFontScale(); this._modalParent = parent; this._modalContext = options.context; this._closeModalCallback = (...originalArgs) => { @@ -859,6 +859,27 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { this.style.accessibilityMediaSession = value; } + get iosAccessibilityAdjustsFontSize(): boolean { + return this.style.iosAccessibilityAdjustsFontSize; + } + set iosAccessibilityAdjustsFontSize(value: boolean) { + this.style.iosAccessibilityAdjustsFontSize = value; + } + + get iosAccessibilityMinFontScale(): number { + return this.style.iosAccessibilityMinFontScale; + } + set iosAccessibilityMinFontScale(value: number) { + this.style.iosAccessibilityMinFontScale = value; + } + + get iosAccessibilityMaxFontScale(): number { + return this.style.iosAccessibilityMaxFontScale; + } + set iosAccessibilityMaxFontScale(value: number) { + this.style.iosAccessibilityMaxFontScale = value; + } + get automationText(): string { return this.accessibilityIdentifier; } diff --git a/packages/core/ui/styling/style-properties.d.ts b/packages/core/ui/styling/style-properties.d.ts index 290aa553f..3c68567c4 100644 --- a/packages/core/ui/styling/style-properties.d.ts +++ b/packages/core/ui/styling/style-properties.d.ts @@ -2,8 +2,8 @@ import { TransformFunctionsInfo } from '../animation'; import { CoreTypes } from '../../core-types'; import { Color } from '../../color'; import { CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; -import { Style } from '../styling/style'; -import { Font, FontStyle, FontWeight } from './font'; +import { Style } from './style'; +import { Font, FontStyleType, FontWeightType } from './font'; import { Background } from './background'; export namespace Length { @@ -95,11 +95,12 @@ export const verticalAlignmentProperty: CssProperty; export const fontFamilyProperty: InheritedCssProperty; -export const fontStyleProperty: InheritedCssProperty; -export const fontWeightProperty: InheritedCssProperty; +export const fontStyleProperty: InheritedCssProperty; +export const fontWeightProperty: InheritedCssProperty; export const backgroundInternalProperty: CssProperty; export const fontInternalProperty: InheritedCssProperty; +export const fontScaleInternalProperty: InheritedCssProperty; export const androidElevationProperty: CssProperty; export const androidDynamicElevationOffsetProperty: CssProperty; diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts index 7e9038fae..a1062d058 100644 --- a/packages/core/ui/styling/style-properties.ts +++ b/packages/core/ui/styling/style-properties.ts @@ -1,12 +1,12 @@ // Types import { unsetValue, CssProperty, CssAnimationProperty, ShorthandProperty, InheritedCssProperty } from '../core/properties'; -import { Style } from '../styling/style'; +import { Style } from './style'; import { Transformation, TransformationValue, TransformFunctionsInfo } from '../animation'; import { Color } from '../../color'; -import { Font, parseFont, FontStyle, FontStyleType, FontWeight, FontWeightType, FontVariationSettings, FontVariationSettingsType } from '../../ui/styling/font'; +import { Font, parseFont, FontStyle, FontStyleType, FontWeight, FontWeightType, FontVariationSettings, FontVariationSettingsType } from './font'; +import { Background } from './background'; import { layout, hasDuplicates } from '../../utils'; -import { Background } from '../../ui/styling/background'; import { radiansToDegrees } from '../../utils/number-utils'; @@ -1326,13 +1326,13 @@ export const fontFamilyProperty = new InheritedCssProperty({ }); fontFamilyProperty.register(Style); -export const fontScaleProperty = new InheritedCssProperty({ - name: '_fontScale', - cssName: '_fontScale', +export const fontScaleInternalProperty = new InheritedCssProperty({ + name: 'fontScaleInternal', + cssName: '_fontScaleInternal', defaultValue: 1.0, valueConverter: (v) => parseFloat(v), }); -fontScaleProperty.register(Style); +fontScaleInternalProperty.register(Style); export const fontSizeProperty = new InheritedCssProperty({ name: 'fontSize', diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 4fa4d0673..01b35f26e 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -108,7 +108,7 @@ export class Style extends Observable implements StyleDefinition { /** * This property ensures inheritance of a11y scale among views. */ - public _fontScale: number; + public fontScaleInternal: number; public backgroundInternal: Background; public rotate: number; @@ -233,6 +233,9 @@ export class Style extends Observable implements StyleDefinition { public accessibilityLanguage: string; public accessibilityMediaSession: boolean; public accessibilityStep: number; + public iosAccessibilityAdjustsFontSize: boolean; + public iosAccessibilityMinFontScale: number; + public iosAccessibilityMaxFontScale: number; public PropertyBag: { new (): { [property: string]: string }; diff --git a/packages/core/ui/text-base/index.ios.ts b/packages/core/ui/text-base/index.ios.ts index 30644e5be..50ee3fb1e 100644 --- a/packages/core/ui/text-base/index.ios.ts +++ b/packages/core/ui/text-base/index.ios.ts @@ -4,11 +4,12 @@ import { CSSShadow } from '../styling/css-shadow'; // Requires import { Font } from '../styling/font'; +import { iosAccessibilityAdjustsFontSizeProperty, iosAccessibilityMaxFontScaleProperty, iosAccessibilityMinFontScaleProperty } from '../../accessibility/accessibility-properties'; import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, lineHeightProperty, maxLinesProperty, resetSymbol } from './text-base-common'; import { Color } from '../../color'; import { FormattedString } from './formatted-string'; import { Span } from './span'; -import { colorProperty, fontInternalProperty, fontScaleProperty, Length } from '../styling/style-properties'; +import { colorProperty, fontInternalProperty, fontScaleInternalProperty, Length } from '../styling/style-properties'; import { isString, isNullOrUndefined } from '../../utils/types'; import { iOSNativeHelper } from '../../utils'; import { Trace } from '../../trace'; @@ -187,29 +188,49 @@ export class TextBase extends TextBaseCommon { if (!(value instanceof Font) || !this.formattedText) { let nativeView = this.nativeTextViewProtected; nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView; - - if (value instanceof Font) { - // Apply a11y font scale if not set - if (value.fontScale !== this.style._fontScale) { - value.fontScale = this.style._fontScale; - } - nativeView.font = value.getUIFont(nativeView.font); - } else { - nativeView.font = value; - } + nativeView.font = value instanceof Font ? value.getUIFont(nativeView.font) : value; } } - [fontScaleProperty.setNative](value: number) { + [fontScaleInternalProperty.setNative](value: number) { const nativeView = this.nativeTextViewProtected instanceof UIButton ? this.nativeTextViewProtected.titleLabel : this.nativeTextViewProtected; const currentFont = this.style.fontInternal || Font.default.withFontSize(nativeView.font.pointSize); - if (currentFont.fontScale !== value) { - const newFont = currentFont.withFontScale(value); - this.style.fontInternal = newFont; + + let finalValue; + if (this.iosAccessibilityAdjustsFontSize) { + finalValue = value; + + if (this.iosAccessibilityMinFontScale && this.iosAccessibilityMinFontScale > value) { + finalValue = this.iosAccessibilityMinFontScale; + } + if (this.iosAccessibilityMaxFontScale && this.iosAccessibilityMaxFontScale < value) { + finalValue = this.iosAccessibilityMaxFontScale; + } + } else { + finalValue = 1.0; + } + + const newFont = currentFont.withFontScale(finalValue); + this.style.fontInternal = newFont; + + // Request layout on font scale as it's not done automatically + if (currentFont.fontScale !== finalValue) { this.requestLayout(); } } + [iosAccessibilityAdjustsFontSizeProperty.setNative](value: boolean) { + this[fontScaleInternalProperty.setNative](this.style.fontScaleInternal); + } + + [iosAccessibilityMinFontScaleProperty.setNative](value: number) { + this[fontScaleInternalProperty.setNative](this.style.fontScaleInternal); + } + + [iosAccessibilityMaxFontScaleProperty.setNative](value: number) { + this[fontScaleInternalProperty.setNative](this.style.fontScaleInternal); + } + [textAlignmentProperty.setNative](value: CoreTypes.TextAlignmentType) { const nativeView = this.nativeTextViewProtected; switch (value) {