mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 11:42:04 +08:00
feat(core): first class a11y support (#8909)
This commit is contained in:

committed by
Nathan Walker

parent
ef9c3b1f5f
commit
c46da3fad9
73
packages/core/accessibility/accessibility-common.ts
Normal file
73
packages/core/accessibility/accessibility-common.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import type { View } from '../ui/core/view';
|
||||
import type { Page } from '../ui/page';
|
||||
import type { AccessibilityBlurEventData, AccessibilityFocusChangedEventData, AccessibilityFocusEventData } from './accessibility-types';
|
||||
|
||||
const lastFocusedViewOnPageKeyName = '__lastFocusedViewOnPage';
|
||||
|
||||
export const accessibilityBlurEvent = 'accessibilityBlur';
|
||||
export const accessibilityFocusEvent = 'accessibilityFocus';
|
||||
export const accessibilityFocusChangedEvent = 'accessibilityFocusChanged';
|
||||
|
||||
/**
|
||||
* Send notification when accessibility focus state changes.
|
||||
* If either receivedFocus or lostFocus is true, 'accessibilityFocusChanged' is send with value true if element received focus
|
||||
* If receivedFocus, 'accessibilityFocus' is send
|
||||
* if lostFocus, 'accessibilityBlur' is send
|
||||
*
|
||||
* @param {View} view
|
||||
* @param {boolean} receivedFocus
|
||||
* @param {boolean} lostFocus
|
||||
*/
|
||||
export function notifyAccessibilityFocusState(view: View, receivedFocus: boolean, lostFocus: boolean): void {
|
||||
if (!receivedFocus && !lostFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.notify({
|
||||
eventName: accessibilityFocusChangedEvent,
|
||||
object: view,
|
||||
value: !!receivedFocus,
|
||||
} as AccessibilityFocusChangedEventData);
|
||||
|
||||
if (receivedFocus) {
|
||||
if (view.page) {
|
||||
view.page[lastFocusedViewOnPageKeyName] = new WeakRef(view);
|
||||
}
|
||||
|
||||
view.notify({
|
||||
eventName: accessibilityFocusEvent,
|
||||
object: view,
|
||||
} as AccessibilityFocusEventData);
|
||||
} else if (lostFocus) {
|
||||
view.notify({
|
||||
eventName: accessibilityBlurEvent,
|
||||
object: view,
|
||||
} as AccessibilityBlurEventData);
|
||||
}
|
||||
}
|
||||
|
||||
export function getLastFocusedViewOnPage(page: Page): View | null {
|
||||
try {
|
||||
const lastFocusedViewRef = page[lastFocusedViewOnPageKeyName] as WeakRef<View>;
|
||||
if (!lastFocusedViewRef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastFocusedView = lastFocusedViewRef.get();
|
||||
if (!lastFocusedView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!lastFocusedView.parent || lastFocusedView.page !== page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return lastFocusedView;
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
delete page[lastFocusedViewOnPageKeyName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
126
packages/core/accessibility/accessibility-css-helper.ts
Normal file
126
packages/core/accessibility/accessibility-css-helper.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { applyCssClass, getRootView } from '../application';
|
||||
import * as 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';
|
||||
|
||||
// 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];
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
|
||||
const rootView = getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyCssClass(rootView, cssClasses, newCssClass);
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => applyCssClass(rootModalView, cssClasses, newCssClass));
|
||||
}
|
||||
|
||||
function applyFontScaleToRootViews(): void {
|
||||
const rootView = getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontScale = getCurrentFontScale();
|
||||
|
||||
rootView.style._fontScale = fontScale;
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => (rootModalView.style._fontScale = 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 = fontScaleMediumCategoryClass;
|
||||
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);
|
||||
}
|
||||
}
|
121
packages/core/accessibility/accessibility-properties.ts
Normal file
121
packages/core/accessibility/accessibility-properties.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { CssProperty, InheritedCssProperty, Property } from '../ui/core/properties';
|
||||
import type { View } from '../ui/core/view';
|
||||
import { booleanConverter } from '../ui/core/view-base';
|
||||
import { Style } from '../ui/styling/style';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-types';
|
||||
|
||||
function makePropertyEnumConverter<T>(enumValues) {
|
||||
return (value: string): T | null => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const [enumKey, enumValue] of Object.entries<T>(enumValues)) {
|
||||
if (typeof enumKey !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enumKey === value || `${enumValue}`.toLowerCase() === `${value}`.toLowerCase()) {
|
||||
return enumValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export const accessibilityEnabledProperty = new CssProperty<Style, boolean>({
|
||||
name: 'accessible',
|
||||
cssName: 'a11y-enabled',
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
accessibilityEnabledProperty.register(Style);
|
||||
|
||||
const accessibilityHiddenPropertyName = 'accessibilityHidden';
|
||||
const accessibilityHiddenCssName = 'a11y-hidden';
|
||||
|
||||
export const accessibilityHiddenProperty = global.isIOS
|
||||
? new InheritedCssProperty({
|
||||
name: accessibilityHiddenPropertyName,
|
||||
cssName: accessibilityHiddenCssName,
|
||||
valueConverter: booleanConverter,
|
||||
})
|
||||
: new CssProperty({
|
||||
name: accessibilityHiddenPropertyName,
|
||||
cssName: accessibilityHiddenCssName,
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
accessibilityHiddenProperty.register(Style);
|
||||
|
||||
export const accessibilityIdentifierProperty = new Property<View, string>({
|
||||
name: 'accessibilityIdentifier',
|
||||
});
|
||||
|
||||
export const accessibilityRoleProperty = new CssProperty<Style, AccessibilityRole>({
|
||||
name: 'accessibilityRole',
|
||||
cssName: 'a11y-role',
|
||||
valueConverter: makePropertyEnumConverter<AccessibilityRole>(AccessibilityRole),
|
||||
});
|
||||
accessibilityRoleProperty.register(Style);
|
||||
|
||||
export const accessibilityStateProperty = new CssProperty<Style, AccessibilityState>({
|
||||
name: 'accessibilityState',
|
||||
cssName: 'a11y-state',
|
||||
valueConverter: makePropertyEnumConverter<AccessibilityState>(AccessibilityState),
|
||||
});
|
||||
accessibilityStateProperty.register(Style);
|
||||
|
||||
export const accessibilityLabelProperty = new Property<View, string>({
|
||||
name: 'accessibilityLabel',
|
||||
});
|
||||
|
||||
export const accessibilityValueProperty = new Property<View, string>({
|
||||
name: 'accessibilityValue',
|
||||
});
|
||||
|
||||
export const accessibilityHintProperty = new Property<View, string>({
|
||||
name: 'accessibilityHint',
|
||||
});
|
||||
|
||||
export const accessibilityLiveRegionProperty = new CssProperty<Style, AccessibilityLiveRegion>({
|
||||
name: 'accessibilityLiveRegion',
|
||||
cssName: 'a11y-live-region',
|
||||
defaultValue: AccessibilityLiveRegion.None,
|
||||
valueConverter: makePropertyEnumConverter<AccessibilityLiveRegion>(AccessibilityLiveRegion),
|
||||
});
|
||||
accessibilityLiveRegionProperty.register(Style);
|
||||
|
||||
export const accessibilityTraitsProperty = new Property<View, AccessibilityTrait | AccessibilityTrait[]>({
|
||||
name: 'accessibilityTraits',
|
||||
});
|
||||
|
||||
export const accessibilityLanguageProperty = new CssProperty<Style, string>({
|
||||
name: 'accessibilityLanguage',
|
||||
cssName: 'a11y-lang',
|
||||
});
|
||||
accessibilityLanguageProperty.register(Style);
|
||||
|
||||
export const accessibilityMediaSessionProperty = new CssProperty({
|
||||
name: 'accessibilityMediaSession',
|
||||
cssName: 'a11y-media-session',
|
||||
});
|
||||
accessibilityMediaSessionProperty.register(Style);
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the accessibilityStep property.
|
||||
*/
|
||||
export const accessibilityStepProperty = new CssProperty<Style, number>({
|
||||
name: 'accessibilityStep',
|
||||
cssName: 'a11y-step',
|
||||
defaultValue: 10,
|
||||
valueConverter: (v): number => {
|
||||
const step = parseFloat(v);
|
||||
|
||||
if (isNaN(step) || step <= 0) {
|
||||
return 10;
|
||||
}
|
||||
|
||||
return step;
|
||||
},
|
||||
});
|
||||
accessibilityStepProperty.register(Style);
|
35
packages/core/accessibility/accessibility-service-common.ts
Normal file
35
packages/core/accessibility/accessibility-service-common.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Observable } from '../data/observable';
|
||||
|
||||
export class SharedA11YObservable extends Observable {
|
||||
accessibilityServiceEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const AccessibilityServiceEnabledPropName = 'accessibilityServiceEnabled';
|
||||
|
||||
export class CommonA11YServiceEnabledObservable extends SharedA11YObservable {
|
||||
constructor(sharedA11YObservable: SharedA11YObservable) {
|
||||
super();
|
||||
|
||||
const ref = new WeakRef(this);
|
||||
let lastValue: boolean;
|
||||
|
||||
function callback() {
|
||||
const self = ref?.get();
|
||||
if (!self) {
|
||||
sharedA11YObservable.off(Observable.propertyChangeEvent, callback);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = !!sharedA11YObservable.accessibilityServiceEnabled;
|
||||
if (newValue !== lastValue) {
|
||||
self.set(AccessibilityServiceEnabledPropName, newValue);
|
||||
lastValue = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
sharedA11YObservable.on(Observable.propertyChangeEvent, callback);
|
||||
|
||||
this.set(AccessibilityServiceEnabledPropName, !!sharedA11YObservable.accessibilityServiceEnabled);
|
||||
}
|
||||
}
|
130
packages/core/accessibility/accessibility-service.android.ts
Normal file
130
packages/core/accessibility/accessibility-service.android.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import * as Application from '../application';
|
||||
import { Observable } from '../data/observable';
|
||||
import { Trace } from '../trace';
|
||||
import * as Utils from '../utils';
|
||||
import { CommonA11YServiceEnabledObservable, SharedA11YObservable } from './accessibility-service-common';
|
||||
|
||||
export function getAndroidAccessibilityManager(): android.view.accessibility.AccessibilityManager | null {
|
||||
const context = Utils.ad.getApplicationContext() as android.content.Context;
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return context.getSystemService(android.content.Context.ACCESSIBILITY_SERVICE) as android.view.accessibility.AccessibilityManager;
|
||||
}
|
||||
|
||||
const accessibilityStateEnabledPropName = 'accessibilityStateEnabled';
|
||||
const touchExplorationStateEnabledPropName = 'touchExplorationStateEnabled';
|
||||
|
||||
class AndroidSharedA11YObservable extends SharedA11YObservable {
|
||||
[accessibilityStateEnabledPropName]: boolean;
|
||||
[touchExplorationStateEnabledPropName]: boolean;
|
||||
|
||||
get accessibilityServiceEnabled(): boolean {
|
||||
return !!this[accessibilityStateEnabledPropName] && !!this[touchExplorationStateEnabledPropName];
|
||||
}
|
||||
|
||||
set accessibilityServiceEnabled(v) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let accessibilityStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener;
|
||||
let touchExplorationStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
|
||||
let sharedA11YObservable: AndroidSharedA11YObservable;
|
||||
|
||||
function updateAccessibilityState(): void {
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
if (!accessibilityManager) {
|
||||
sharedA11YObservable.set(accessibilityStateEnabledPropName, false);
|
||||
sharedA11YObservable.set(touchExplorationStateEnabledPropName, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sharedA11YObservable.set(accessibilityStateEnabledPropName, !!accessibilityManager.isEnabled());
|
||||
sharedA11YObservable.set(touchExplorationStateEnabledPropName, !!accessibilityManager.isTouchExplorationEnabled());
|
||||
}
|
||||
|
||||
function ensureStateListener(): SharedA11YObservable {
|
||||
if (sharedA11YObservable) {
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
sharedA11YObservable = new AndroidSharedA11YObservable();
|
||||
|
||||
if (!accessibilityManager) {
|
||||
sharedA11YObservable.set(accessibilityStateEnabledPropName, false);
|
||||
sharedA11YObservable.set(touchExplorationStateEnabledPropName, false);
|
||||
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
accessibilityStateChangeListener = new android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener({
|
||||
onAccessibilityStateChanged(enabled) {
|
||||
updateAccessibilityState();
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`AccessibilityStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
touchExplorationStateChangeListener = new androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener({
|
||||
onTouchExplorationStateChanged(enabled) {
|
||||
updateAccessibilityState();
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`TouchExplorationStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
|
||||
androidx.core.view.accessibility.AccessibilityManagerCompat.addTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
|
||||
|
||||
updateAccessibilityState();
|
||||
|
||||
Application.on(Application.resumeEvent, updateAccessibilityState);
|
||||
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
return ensureStateListener().accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
Application.on(Application.exitEvent, (args: Application.ApplicationEventData) => {
|
||||
const activity = args.android as android.app.Activity;
|
||||
if (activity && !activity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
if (accessibilityManager) {
|
||||
if (accessibilityStateChangeListener) {
|
||||
accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
|
||||
}
|
||||
|
||||
if (touchExplorationStateChangeListener) {
|
||||
androidx.core.view.accessibility.AccessibilityManagerCompat.removeTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
accessibilityStateChangeListener = null;
|
||||
touchExplorationStateChangeListener = null;
|
||||
|
||||
if (sharedA11YObservable) {
|
||||
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
|
||||
sharedA11YObservable = null;
|
||||
}
|
||||
|
||||
Application.off(Application.resumeEvent, updateAccessibilityState);
|
||||
});
|
||||
|
||||
export class AccessibilityServiceEnabledObservable extends CommonA11YServiceEnabledObservable {
|
||||
constructor() {
|
||||
super(ensureStateListener());
|
||||
}
|
||||
}
|
10
packages/core/accessibility/accessibility-service.d.ts
vendored
Normal file
10
packages/core/accessibility/accessibility-service.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import { Observable } from '../data/observable';
|
||||
|
||||
export class AccessibilityServiceEnabledObservable extends Observable {
|
||||
public readonly accessibilityServiceEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Android platform's AccessibilityManager.
|
||||
*/
|
||||
export function getAndroidAccessibilityManager(): /* android.view.accessibility.AccessibilityManager */ any | null;
|
75
packages/core/accessibility/accessibility-service.ios.ts
Normal file
75
packages/core/accessibility/accessibility-service.ios.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import * as Application from '../application';
|
||||
import { Observable } from '../data/observable';
|
||||
import { Trace } from '../trace';
|
||||
import { AccessibilityServiceEnabledPropName, CommonA11YServiceEnabledObservable, SharedA11YObservable } from './accessibility-service-common';
|
||||
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
return getSharedA11YObservable().accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
export function getAndroidAccessibilityManager(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sharedA11YObservable: SharedA11YObservable;
|
||||
let nativeObserver;
|
||||
|
||||
function getSharedA11YObservable(): SharedA11YObservable {
|
||||
if (sharedA11YObservable) {
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
sharedA11YObservable = new SharedA11YObservable();
|
||||
|
||||
let isVoiceOverRunning: () => boolean;
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning === 'function') {
|
||||
isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning;
|
||||
} else {
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning !== 'function') {
|
||||
Trace.write(`UIAccessibilityIsVoiceOverRunning() - is not a function`, Trace.categories.Accessibility, Trace.messageType.error);
|
||||
|
||||
isVoiceOverRunning = () => false;
|
||||
}
|
||||
}
|
||||
|
||||
sharedA11YObservable.set(AccessibilityServiceEnabledPropName, isVoiceOverRunning());
|
||||
|
||||
let voiceOverStatusChangedNotificationName: string | null = null;
|
||||
if (typeof UIAccessibilityVoiceOverStatusDidChangeNotification !== 'undefined') {
|
||||
// iOS 11+
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusDidChangeNotification;
|
||||
} else if (typeof UIAccessibilityVoiceOverStatusChanged !== 'undefined') {
|
||||
// iOS <11
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusChanged;
|
||||
}
|
||||
|
||||
if (voiceOverStatusChangedNotificationName) {
|
||||
nativeObserver = Application.ios.addNotificationObserver(voiceOverStatusChangedNotificationName, () => {
|
||||
sharedA11YObservable?.set(AccessibilityServiceEnabledPropName, isVoiceOverRunning());
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeObserver, voiceOverStatusChangedNotificationName);
|
||||
}
|
||||
|
||||
nativeObserver = null;
|
||||
|
||||
if (sharedA11YObservable) {
|
||||
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
|
||||
|
||||
sharedA11YObservable = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Application.on(Application.resumeEvent, () => sharedA11YObservable.set(AccessibilityServiceEnabledPropName, isVoiceOverRunning()));
|
||||
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
export class AccessibilityServiceEnabledObservable extends CommonA11YServiceEnabledObservable {
|
||||
constructor() {
|
||||
super(getSharedA11YObservable());
|
||||
}
|
||||
}
|
326
packages/core/accessibility/accessibility-types.ts
Normal file
326
packages/core/accessibility/accessibility-types.ts
Normal file
@ -0,0 +1,326 @@
|
||||
import type { EventData } from '../data/observable';
|
||||
import type { View } from '../ui/core/view';
|
||||
|
||||
export enum AccessibilityTrait {
|
||||
/**
|
||||
* The element has no traits.
|
||||
*/
|
||||
None = 'none',
|
||||
|
||||
/**
|
||||
* The element should be treated as a button.
|
||||
*/
|
||||
Button = 'button',
|
||||
|
||||
/**
|
||||
* The element should be treated as a link.
|
||||
*/
|
||||
Link = 'link',
|
||||
|
||||
/**
|
||||
* The element should be treated as a search field.
|
||||
*/
|
||||
SearchField = 'search',
|
||||
|
||||
/**
|
||||
* The element should be treated as an image.
|
||||
*/
|
||||
Image = 'image',
|
||||
|
||||
/**
|
||||
* The element is currently selected.
|
||||
*/
|
||||
Selected = 'selected',
|
||||
|
||||
/**
|
||||
* The element plays its own sound when activated.
|
||||
*/
|
||||
PlaysSound = 'plays',
|
||||
|
||||
/**
|
||||
* The element behaves as a keyboard key.
|
||||
*/
|
||||
KeyboardKey = 'key',
|
||||
|
||||
/**
|
||||
* The element should be treated as static text that cannot change.
|
||||
*/
|
||||
StaticText = 'text',
|
||||
|
||||
/**
|
||||
* The element provides summary information when the application starts.
|
||||
*/
|
||||
SummaryElement = 'summary',
|
||||
|
||||
/**
|
||||
* The element is not enabled and does not respond to user interaction.
|
||||
*/
|
||||
NotEnabled = 'disabled',
|
||||
|
||||
/**
|
||||
* The element frequently updates its label or value.
|
||||
*/
|
||||
UpdatesFrequently = 'frequentUpdates',
|
||||
|
||||
/**
|
||||
* The element starts a media session when it is activated.
|
||||
*/
|
||||
StartsMediaSession = 'startsMedia',
|
||||
|
||||
/**
|
||||
* The element allows continuous adjustment through a range of values.
|
||||
*/
|
||||
Adjustable = 'adjustable',
|
||||
|
||||
/**
|
||||
* The element allows direct touch interaction for VoiceOver users.
|
||||
*/
|
||||
AllowsDirectInteraction = 'allowsDirectInteraction',
|
||||
|
||||
/**
|
||||
* The element should cause an automatic page turn when VoiceOver finishes reading the text within it.
|
||||
* Note: Requires custom view with accessibilityScroll(...)
|
||||
*/
|
||||
CausesPageTurn = 'pageTurn',
|
||||
|
||||
/**
|
||||
* The element is a header that divides content into sections, such as the title of a navigation bar.
|
||||
*/
|
||||
Header = 'header',
|
||||
}
|
||||
|
||||
export enum AccessibilityRole {
|
||||
/**
|
||||
* The element has no traits.
|
||||
*/
|
||||
None = 'none',
|
||||
|
||||
/**
|
||||
* The element should be treated as a button.
|
||||
*/
|
||||
Button = 'button',
|
||||
|
||||
/**
|
||||
* The element should be treated as a link.
|
||||
*/
|
||||
Link = 'link',
|
||||
|
||||
/**
|
||||
* The element should be treated as a search field.
|
||||
*/
|
||||
Search = 'search',
|
||||
|
||||
/**
|
||||
* The element should be treated as an image.
|
||||
*/
|
||||
Image = 'image',
|
||||
|
||||
/**
|
||||
* The element should be treated as a image button.
|
||||
*/
|
||||
ImageButton = 'imageButton',
|
||||
|
||||
/**
|
||||
* The element behaves as a keyboard key.
|
||||
*/
|
||||
KeyboardKey = 'keyboardKey',
|
||||
|
||||
/**
|
||||
* The element should be treated as static text that cannot change.
|
||||
*/
|
||||
StaticText = 'textField',
|
||||
|
||||
/**
|
||||
* The element allows continuous adjustment through a range of values.
|
||||
*/
|
||||
Adjustable = 'adjustable',
|
||||
|
||||
/**
|
||||
* The element provides summary information when the application starts.
|
||||
*/
|
||||
Summary = 'summary',
|
||||
|
||||
/**
|
||||
* The element is a header that divides content into sections, such as the title of a navigation bar.
|
||||
*/
|
||||
Header = 'header',
|
||||
|
||||
/**
|
||||
* The element behaves like a Checkbox
|
||||
*/
|
||||
Checkbox = 'checkbox',
|
||||
|
||||
/**
|
||||
* The element behaves like a ProgressBar
|
||||
*/
|
||||
ProgressBar = 'progressBar',
|
||||
|
||||
/**
|
||||
* The element behaves like a RadioButton
|
||||
*/
|
||||
RadioButton = 'radioButton',
|
||||
|
||||
/**
|
||||
* The element behaves like a SpinButton
|
||||
*/
|
||||
SpinButton = 'spinButton',
|
||||
|
||||
/**
|
||||
* The element behaves like a switch
|
||||
*/
|
||||
Switch = 'switch',
|
||||
}
|
||||
|
||||
export enum AccessibilityState {
|
||||
Selected = 'selected',
|
||||
Checked = 'checked',
|
||||
Unchecked = 'unchecked',
|
||||
Disabled = 'disabled',
|
||||
}
|
||||
|
||||
export enum AccessibilityLiveRegion {
|
||||
None = 'none',
|
||||
Polite = 'polite',
|
||||
Assertive = 'assertive',
|
||||
}
|
||||
|
||||
export interface AccessibilityFocusEventData extends EventData {
|
||||
object: View;
|
||||
}
|
||||
|
||||
export type AccessibilityBlurEventData = AccessibilityFocusEventData;
|
||||
|
||||
export interface AccessibilityFocusChangedEventData extends AccessibilityFocusEventData {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export enum IOSPostAccessibilityNotificationType {
|
||||
Announcement = 'announcement',
|
||||
Screen = 'screen',
|
||||
Layout = 'layout',
|
||||
}
|
||||
|
||||
export enum AndroidAccessibilityEvent {
|
||||
/**
|
||||
* Invalid selection/focus position.
|
||||
*/
|
||||
INVALID_POSITION = 'invalid_position',
|
||||
|
||||
/**
|
||||
* Maximum length of the text fields.
|
||||
*/
|
||||
MAX_TEXT_LENGTH = 'max_text_length',
|
||||
|
||||
/**
|
||||
* Represents the event of clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
|
||||
*/
|
||||
VIEW_CLICKED = 'view_clicked',
|
||||
|
||||
/**
|
||||
* Represents the event of long clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
|
||||
*/
|
||||
VIEW_LONG_CLICKED = 'view_long_clicked',
|
||||
|
||||
/**
|
||||
* Represents the event of selecting an item usually in the context of an android.widget.AdapterView.
|
||||
*/
|
||||
VIEW_SELECTED = 'view_selected',
|
||||
|
||||
/**
|
||||
* Represents the event of setting input focus of a android.view.View.
|
||||
*/
|
||||
VIEW_FOCUSED = 'view_focused',
|
||||
|
||||
/**
|
||||
* Represents the event of changing the text of an android.widget.EditText.
|
||||
*/
|
||||
VIEW_TEXT_CHANGED = 'view_text_changed',
|
||||
|
||||
/**
|
||||
* Represents the event of opening a android.widget.PopupWindow, android.view.Menu, android.app.Dialog, etc.
|
||||
*/
|
||||
WINDOW_STATE_CHANGED = 'window_state_changed',
|
||||
|
||||
/**
|
||||
* Represents the event showing a android.app.Notification.
|
||||
*/
|
||||
NOTIFICATION_STATE_CHANGED = 'notification_state_changed',
|
||||
|
||||
/**
|
||||
* Represents the event of a hover enter over a android.view.View.
|
||||
*/
|
||||
VIEW_HOVER_ENTER = 'view_hover_enter',
|
||||
|
||||
/**
|
||||
* Represents the event of a hover exit over a android.view.View.
|
||||
*/
|
||||
VIEW_HOVER_EXIT = 'view_hover_exit',
|
||||
/**
|
||||
* Represents the event of starting a touch exploration gesture.
|
||||
*/
|
||||
TOUCH_EXPLORATION_GESTURE_START = 'touch_exploration_gesture_start',
|
||||
/**
|
||||
* Represents the event of ending a touch exploration gesture.
|
||||
*/
|
||||
TOUCH_EXPLORATION_GESTURE_END = 'touch_exploration_gesture_end',
|
||||
|
||||
/**
|
||||
* Represents the event of changing the content of a window and more specifically the sub-tree rooted at the event's source.
|
||||
*/
|
||||
WINDOW_CONTENT_CHANGED = 'window_content_changed',
|
||||
|
||||
/**
|
||||
* Represents the event of scrolling a view.
|
||||
*/
|
||||
VIEW_SCROLLED = 'view_scrolled',
|
||||
|
||||
/**
|
||||
* Represents the event of changing the selection in an android.widget.EditText.
|
||||
*/
|
||||
VIEW_TEXT_SELECTION_CHANGED = 'view_text_selection_changed',
|
||||
|
||||
/**
|
||||
* Represents the event of an application making an announcement.
|
||||
*/
|
||||
ANNOUNCEMENT = 'announcement',
|
||||
|
||||
/**
|
||||
* Represents the event of gaining accessibility focus.
|
||||
*/
|
||||
VIEW_ACCESSIBILITY_FOCUSED = 'view_accessibility_focused',
|
||||
|
||||
/**
|
||||
* Represents the event of clearing accessibility focus.
|
||||
*/
|
||||
VIEW_ACCESSIBILITY_FOCUS_CLEARED = 'view_accessibility_focus_cleared',
|
||||
|
||||
/**
|
||||
* Represents the event of traversing the text of a view at a given movement granularity.
|
||||
*/
|
||||
VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 'view_text_traversed_at_movement_granularity',
|
||||
|
||||
/**
|
||||
* Represents the event of beginning gesture detection.
|
||||
*/
|
||||
GESTURE_DETECTION_START = 'gesture_detection_start',
|
||||
|
||||
/**
|
||||
* Represents the event of ending gesture detection.
|
||||
*/
|
||||
GESTURE_DETECTION_END = 'gesture_detection_end',
|
||||
|
||||
/**
|
||||
* Represents the event of the user starting to touch the screen.
|
||||
*/
|
||||
TOUCH_INTERACTION_START = 'touch_interaction_start',
|
||||
|
||||
/**
|
||||
* Represents the event of the user ending to touch the screen.
|
||||
*/
|
||||
TOUCH_INTERACTION_END = 'touch_interaction_end',
|
||||
|
||||
/**
|
||||
* Mask for AccessibilityEvent all types.
|
||||
*/
|
||||
ALL_MASK = 'all',
|
||||
}
|
15
packages/core/accessibility/font-scale-common.ts
Normal file
15
packages/core/accessibility/font-scale-common.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const VALID_FONT_SCALES = global.isIOS // iOS supports a wider number of font scales than Android does.
|
||||
? [0.5, 0.7, 0.85, 1, 1.15, 1.3, 1.5, 2, 2.5, 3, 3.5, 4]
|
||||
: [0.85, 1, 1.15, 1.3];
|
||||
|
||||
export function getClosestValidFontScale(fontScale: number): number {
|
||||
fontScale = Number(fontScale) || 1;
|
||||
|
||||
return VALID_FONT_SCALES.sort((a, b) => Math.abs(fontScale - a) - Math.abs(fontScale - b)).shift();
|
||||
}
|
||||
|
||||
export enum FontScaleCategory {
|
||||
ExtraSmall = 'extra-small',
|
||||
Medium = 'medium',
|
||||
ExtraLarge = 'extra-large',
|
||||
}
|
66
packages/core/accessibility/font-scale.android.ts
Normal file
66
packages/core/accessibility/font-scale.android.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import * as Application from '../application';
|
||||
import { FontScaleCategory, getClosestValidFontScale } from './font-scale-common';
|
||||
export * from './font-scale-common';
|
||||
|
||||
let currentFontScale: number = null;
|
||||
function fontScaleChanged(origFontScale: number) {
|
||||
const oldValue = currentFontScale;
|
||||
currentFontScale = getClosestValidFontScale(origFontScale);
|
||||
|
||||
if (oldValue !== currentFontScale) {
|
||||
Application.notify({
|
||||
eventName: Application.fontScaleChangedEvent,
|
||||
object: Application,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentFontScale(): number {
|
||||
setupConfigListener();
|
||||
|
||||
return currentFontScale;
|
||||
}
|
||||
|
||||
export function getFontScaleCategory(): FontScaleCategory {
|
||||
return FontScaleCategory.Medium;
|
||||
}
|
||||
|
||||
function useAndroidFontScale() {
|
||||
fontScaleChanged(Number(Application.android.context.getResources().getConfiguration().fontScale));
|
||||
}
|
||||
|
||||
let configChangedCallback: android.content.ComponentCallbacks2;
|
||||
function setupConfigListener() {
|
||||
if (configChangedCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
Application.off(Application.launchEvent, setupConfigListener);
|
||||
const context = Application.android?.context as android.content.Context;
|
||||
if (!context) {
|
||||
Application.on(Application.launchEvent, setupConfigListener);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
useAndroidFontScale();
|
||||
|
||||
configChangedCallback = new android.content.ComponentCallbacks2({
|
||||
onLowMemory() {
|
||||
// Dummy
|
||||
},
|
||||
onTrimMemory() {
|
||||
// Dummy
|
||||
},
|
||||
onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
fontScaleChanged(Number(newConfig.fontScale));
|
||||
},
|
||||
});
|
||||
|
||||
context.registerComponentCallbacks(configChangedCallback);
|
||||
Application.on(Application.resumeEvent, useAndroidFontScale);
|
||||
}
|
||||
|
||||
export function initAccessibilityFontScale(): void {
|
||||
setupConfigListener();
|
||||
}
|
9
packages/core/accessibility/font-scale.d.ts
vendored
Normal file
9
packages/core/accessibility/font-scale.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
export const VALID_FONT_SCALES: number[];
|
||||
export function getCurrentFontScale(): number;
|
||||
export enum FontScaleCategory {
|
||||
ExtraSmall = 'extra-small',
|
||||
Medium = 'medium',
|
||||
ExtraLarge = 'extra-large',
|
||||
}
|
||||
export function getFontScaleCategory(): FontScaleCategory;
|
||||
export function initAccessibilityFontScale(): void;
|
109
packages/core/accessibility/font-scale.ios.ts
Normal file
109
packages/core/accessibility/font-scale.ios.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import * as Application from '../application';
|
||||
import { FontScaleCategory, getClosestValidFontScale } from './font-scale-common';
|
||||
export * from './font-scale-common';
|
||||
|
||||
let currentFontScale: number = null;
|
||||
function fontScaleChanged(origFontScale: number) {
|
||||
const oldValue = currentFontScale;
|
||||
currentFontScale = getClosestValidFontScale(origFontScale);
|
||||
|
||||
if (oldValue !== currentFontScale) {
|
||||
Application.notify({
|
||||
eventName: Application.fontScaleChangedEvent,
|
||||
object: Application,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentFontScale(): number {
|
||||
setupConfigListener();
|
||||
|
||||
return currentFontScale;
|
||||
}
|
||||
|
||||
export function getFontScaleCategory(): FontScaleCategory {
|
||||
if (currentFontScale < 0.85) {
|
||||
return FontScaleCategory.ExtraSmall;
|
||||
}
|
||||
|
||||
if (currentFontScale > 1.5) {
|
||||
return FontScaleCategory.ExtraLarge;
|
||||
}
|
||||
|
||||
return FontScaleCategory.Medium;
|
||||
}
|
||||
|
||||
const sizeMap = new Map<string, number>([
|
||||
[UIContentSizeCategoryExtraSmall, 0.5],
|
||||
[UIContentSizeCategorySmall, 0.7],
|
||||
[UIContentSizeCategoryMedium, 0.85],
|
||||
[UIContentSizeCategoryLarge, 1],
|
||||
[UIContentSizeCategoryExtraLarge, 1.15],
|
||||
[UIContentSizeCategoryExtraExtraLarge, 1.3],
|
||||
[UIContentSizeCategoryExtraExtraExtraLarge, 1.5],
|
||||
[UIContentSizeCategoryAccessibilityMedium, 2],
|
||||
[UIContentSizeCategoryAccessibilityLarge, 2.5],
|
||||
[UIContentSizeCategoryAccessibilityExtraLarge, 3],
|
||||
[UIContentSizeCategoryAccessibilityExtraExtraLarge, 3.5],
|
||||
[UIContentSizeCategoryAccessibilityExtraExtraExtraLarge, 4],
|
||||
]);
|
||||
|
||||
function contentSizeUpdated(fontSize: string) {
|
||||
if (sizeMap.has(fontSize)) {
|
||||
fontScaleChanged(sizeMap.get(fontSize));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fontScaleChanged(1);
|
||||
}
|
||||
|
||||
function useIOSFontScale() {
|
||||
if (Application.ios.nativeApp) {
|
||||
contentSizeUpdated(Application.ios.nativeApp.preferredContentSizeCategory);
|
||||
} else {
|
||||
fontScaleChanged(1);
|
||||
}
|
||||
}
|
||||
|
||||
let fontSizeObserver;
|
||||
function setupConfigListener(attempt = 0) {
|
||||
if (fontSizeObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Application.ios.nativeApp) {
|
||||
if (attempt > 100) {
|
||||
fontScaleChanged(1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Couldn't get launchEvent to trigger.
|
||||
setTimeout(() => setupConfigListener(attempt + 1), 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fontSizeObserver = Application.ios.addNotificationObserver(UIContentSizeCategoryDidChangeNotification, (args) => {
|
||||
const fontSize = args.userInfo.valueForKey(UIContentSizeCategoryNewValueKey);
|
||||
contentSizeUpdated(fontSize);
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (fontSizeObserver) {
|
||||
Application.ios.removeNotificationObserver(fontSizeObserver, UIContentSizeCategoryDidChangeNotification);
|
||||
fontSizeObserver = null;
|
||||
}
|
||||
|
||||
Application.off(Application.resumeEvent, useIOSFontScale);
|
||||
});
|
||||
|
||||
Application.on(Application.resumeEvent, useIOSFontScale);
|
||||
|
||||
useIOSFontScale();
|
||||
}
|
||||
|
||||
export function initAccessibilityFontScale(): void {
|
||||
setupConfigListener();
|
||||
}
|
667
packages/core/accessibility/index.android.ts
Normal file
667
packages/core/accessibility/index.android.ts
Normal file
@ -0,0 +1,667 @@
|
||||
import * as Application from '../application';
|
||||
import { Trace } from '../trace';
|
||||
import type { View } from '../ui/core/view';
|
||||
import { GestureTypes } from '../ui/gestures';
|
||||
import { notifyAccessibilityFocusState } from './accessibility-common';
|
||||
import { getAndroidAccessibilityManager } from './accessibility-service';
|
||||
import { AccessibilityRole, AccessibilityState, AndroidAccessibilityEvent } from './accessibility-types';
|
||||
|
||||
export * from './accessibility-common';
|
||||
export * from './accessibility-types';
|
||||
export * from './font-scale';
|
||||
|
||||
let clickableRolesMap = new Set<string>();
|
||||
|
||||
let lastFocusedView: WeakRef<View>;
|
||||
function accessibilityEventHelper(view: View, eventType: number) {
|
||||
const eventName = accessibilityEventTypeMap.get(eventType);
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`accessibilityEventHelper: Service not active`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventName) {
|
||||
Trace.write(`accessibilityEventHelper: unknown eventType: ${eventType}`, Trace.categories.Accessibility, Trace.messageType.error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`accessibilityEventHelper: no owner: ${eventName}`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const androidView = view.nativeViewProtected as android.view.View;
|
||||
if (!androidView) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`accessibilityEventHelper: no nativeView`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventType) {
|
||||
case android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED: {
|
||||
/**
|
||||
* Android API >= 26 handles accessibility tap-events by converting them to TYPE_VIEW_CLICKED
|
||||
* These aren't triggered for custom tap events in NativeScript.
|
||||
*/
|
||||
if (android.os.Build.VERSION.SDK_INT >= 26) {
|
||||
// Find all tap gestures and trigger them.
|
||||
for (const tapGesture of view.getGestureObservers(GestureTypes.tap) ?? []) {
|
||||
tapGesture.callback({
|
||||
android: view.android,
|
||||
eventName: 'tap',
|
||||
ios: null,
|
||||
object: view,
|
||||
type: GestureTypes.tap,
|
||||
view: view,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
|
||||
const lastView = lastFocusedView?.get();
|
||||
if (lastView && view !== lastView) {
|
||||
const lastAndroidView = lastView.nativeViewProtected as android.view.View;
|
||||
if (lastAndroidView) {
|
||||
lastAndroidView.clearFocus();
|
||||
lastFocusedView = null;
|
||||
|
||||
notifyAccessibilityFocusState(lastView, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
lastFocusedView = new WeakRef(view);
|
||||
|
||||
notifyAccessibilityFocusState(view, true, false);
|
||||
|
||||
return;
|
||||
}
|
||||
case android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
|
||||
const lastView = lastFocusedView?.get();
|
||||
if (lastView && view === lastView) {
|
||||
lastFocusedView = null;
|
||||
androidView.clearFocus();
|
||||
}
|
||||
|
||||
notifyAccessibilityFocusState(view, false, true);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let TNSAccessibilityDelegate: android.view.View.androidviewViewAccessibilityDelegate;
|
||||
|
||||
const androidViewToTNSView = new WeakMap<android.view.View, WeakRef<View>>();
|
||||
|
||||
let accessibilityEventMap: Map<AndroidAccessibilityEvent, number>;
|
||||
let accessibilityEventTypeMap: Map<number, string>;
|
||||
|
||||
function ensureNativeClasses() {
|
||||
if (TNSAccessibilityDelegate) {
|
||||
return;
|
||||
}
|
||||
|
||||
// WORKAROUND: Typing refers to android.view.View.androidviewViewAccessibilityDelegate but it is called android.view.View.AccessibilityDelegate at runtime
|
||||
const AccessibilityDelegate: typeof android.view.View.androidviewViewAccessibilityDelegate = android.view.View['AccessibilityDelegate'];
|
||||
|
||||
const RoleTypeMap = new Map<AccessibilityRole, string>([
|
||||
[AccessibilityRole.Button, android.widget.Button.class.getName()],
|
||||
[AccessibilityRole.Search, android.widget.EditText.class.getName()],
|
||||
[AccessibilityRole.Image, android.widget.ImageView.class.getName()],
|
||||
[AccessibilityRole.ImageButton, android.widget.ImageButton.class.getName()],
|
||||
[AccessibilityRole.KeyboardKey, android.inputmethodservice.Keyboard.Key.class.getName()],
|
||||
[AccessibilityRole.StaticText, android.widget.TextView.class.getName()],
|
||||
[AccessibilityRole.Adjustable, android.widget.SeekBar.class.getName()],
|
||||
[AccessibilityRole.Checkbox, android.widget.CheckBox.class.getName()],
|
||||
[AccessibilityRole.RadioButton, android.widget.RadioButton.class.getName()],
|
||||
[AccessibilityRole.SpinButton, android.widget.Spinner.class.getName()],
|
||||
[AccessibilityRole.Switch, android.widget.Switch.class.getName()],
|
||||
[AccessibilityRole.ProgressBar, android.widget.ProgressBar.class.getName()],
|
||||
]);
|
||||
|
||||
clickableRolesMap = new Set<string>([AccessibilityRole.Button, AccessibilityRole.ImageButton]);
|
||||
|
||||
const ignoreRoleTypesForTrace = new Set([AccessibilityRole.Header, AccessibilityRole.Link, AccessibilityRole.None, AccessibilityRole.Summary]);
|
||||
|
||||
@NativeClass()
|
||||
class TNSAccessibilityDelegateImpl extends AccessibilityDelegate {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
return global.__native(this);
|
||||
}
|
||||
|
||||
private getTnsView(androidView: android.view.View) {
|
||||
const view = androidViewToTNSView.get(androidView)?.get();
|
||||
if (!view) {
|
||||
androidViewToTNSView.delete(androidView);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public onInitializeAccessibilityNodeInfo(host: android.view.View, info: android.view.accessibility.AccessibilityNodeInfo) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
|
||||
const view = this.getTnsView(host);
|
||||
if (!view) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${host} ${info} no tns-view`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const accessibilityRole = view.accessibilityRole;
|
||||
if (accessibilityRole) {
|
||||
const androidClassName = RoleTypeMap.get(accessibilityRole);
|
||||
if (androidClassName) {
|
||||
const oldClassName = info.getClassName() || (android.os.Build.VERSION.SDK_INT >= 28 && host.getAccessibilityClassName()) || null;
|
||||
info.setClassName(androidClassName);
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${view}.accessibilityRole = "${accessibilityRole}" is mapped to "${androidClassName}" (was ${oldClassName}). ${info.getClassName()}`, Trace.categories.Accessibility);
|
||||
}
|
||||
} else if (!ignoreRoleTypesForTrace.has(accessibilityRole)) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${view}.accessibilityRole = "${accessibilityRole}" is unknown`, Trace.categories.Accessibility);
|
||||
}
|
||||
}
|
||||
|
||||
if (clickableRolesMap.has(accessibilityRole)) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set clickable role=${accessibilityRole}`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
info.setClickable(true);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
if (accessibilityRole === AccessibilityRole.Header) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set heading role=${accessibilityRole}`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
info.setHeading(true);
|
||||
} else if (host.isAccessibilityHeading()) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set heading from host`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
info.setHeading(true);
|
||||
} else {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set not heading`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
info.setHeading(false);
|
||||
}
|
||||
}
|
||||
|
||||
switch (accessibilityRole) {
|
||||
case AccessibilityRole.Switch:
|
||||
case AccessibilityRole.RadioButton:
|
||||
case AccessibilityRole.Checkbox: {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set checkable and check=${view.accessibilityState === AccessibilityState.Checked}`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
info.setCheckable(true);
|
||||
info.setChecked(view.accessibilityState === AccessibilityState.Checked);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`onInitializeAccessibilityNodeInfo ${view} - set enabled=${view.accessibilityState !== AccessibilityState.Disabled} and selected=${view.accessibilityState === AccessibilityState.Selected}`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
info.setEnabled(view.accessibilityState !== AccessibilityState.Disabled);
|
||||
info.setSelected(view.accessibilityState === AccessibilityState.Selected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (view.accessible === true) {
|
||||
info.setFocusable(true);
|
||||
}
|
||||
}
|
||||
|
||||
public sendAccessibilityEvent(host: android.view.ViewGroup, eventType: number) {
|
||||
super.sendAccessibilityEvent(host, eventType);
|
||||
const view = this.getTnsView(host);
|
||||
if (!view) {
|
||||
console.log(`skip - ${host} - ${accessibilityEventTypeMap.get(eventType)}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
accessibilityEventHelper(view, eventType);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TNSAccessibilityDelegate = new TNSAccessibilityDelegateImpl();
|
||||
|
||||
accessibilityEventMap = new Map<AndroidAccessibilityEvent, number>([
|
||||
/**
|
||||
* Invalid selection/focus position.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.INVALID_POSITION, android.view.accessibility.AccessibilityEvent.INVALID_POSITION],
|
||||
/**
|
||||
* Maximum length of the text fields.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.MAX_TEXT_LENGTH, android.view.accessibility.AccessibilityEvent.MAX_TEXT_LENGTH],
|
||||
/**
|
||||
* Represents the event of clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_CLICKED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_CLICKED],
|
||||
/**
|
||||
* Represents the event of long clicking on a android.view.View like android.widget.Button, android.widget.CompoundButton, etc.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_LONG_CLICKED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_LONG_CLICKED],
|
||||
/**
|
||||
* Represents the event of selecting an item usually in the context of an android.widget.AdapterView.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_SELECTED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SELECTED],
|
||||
/**
|
||||
* Represents the event of setting input focus of a android.view.View.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_FOCUSED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED],
|
||||
/**
|
||||
* Represents the event of changing the text of an android.widget.EditText.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_TEXT_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED],
|
||||
/**
|
||||
* Represents the event of opening a android.widget.PopupWindow, android.view.Menu, android.app.Dialog, etc.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.WINDOW_STATE_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED],
|
||||
/**
|
||||
* Represents the event showing a android.app.Notification.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.NOTIFICATION_STATE_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED],
|
||||
/**
|
||||
* Represents the event of a hover enter over a android.view.View.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_HOVER_ENTER, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_ENTER],
|
||||
/**
|
||||
* Represents the event of a hover exit over a android.view.View.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_HOVER_EXIT, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_HOVER_EXIT],
|
||||
/**
|
||||
* Represents the event of starting a touch exploration gesture.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.TOUCH_EXPLORATION_GESTURE_START, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START],
|
||||
/**
|
||||
* Represents the event of ending a touch exploration gesture.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.TOUCH_EXPLORATION_GESTURE_END, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END],
|
||||
/**
|
||||
* Represents the event of changing the content of a window and more specifically the sub-tree rooted at the event's source.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.WINDOW_CONTENT_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED],
|
||||
/**
|
||||
* Represents the event of scrolling a view.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_SCROLLED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED],
|
||||
/**
|
||||
* Represents the event of changing the selection in an android.widget.EditText.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_TEXT_SELECTION_CHANGED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED],
|
||||
/**
|
||||
* Represents the event of an application making an announcement.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.ANNOUNCEMENT, android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT],
|
||||
/**
|
||||
* Represents the event of gaining accessibility focus.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_ACCESSIBILITY_FOCUSED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED],
|
||||
/**
|
||||
* Represents the event of clearing accessibility focus.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_ACCESSIBILITY_FOCUS_CLEARED, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED],
|
||||
/**
|
||||
* Represents the event of traversing the text of a view at a given movement granularity.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, android.view.accessibility.AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY],
|
||||
/**
|
||||
* Represents the event of beginning gesture detection.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.GESTURE_DETECTION_START, android.view.accessibility.AccessibilityEvent.TYPE_GESTURE_DETECTION_START],
|
||||
/**
|
||||
* Represents the event of ending gesture detection.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.GESTURE_DETECTION_END, android.view.accessibility.AccessibilityEvent.TYPE_GESTURE_DETECTION_END],
|
||||
/**
|
||||
* Represents the event of the user starting to touch the screen.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.TOUCH_INTERACTION_START, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_START],
|
||||
/**
|
||||
* Represents the event of the user ending to touch the screen.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.TOUCH_INTERACTION_END, android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END],
|
||||
/**
|
||||
* Mask for AccessibilityEvent all types.
|
||||
*/
|
||||
[AndroidAccessibilityEvent.ALL_MASK, android.view.accessibility.AccessibilityEvent.TYPES_ALL_MASK],
|
||||
]);
|
||||
|
||||
accessibilityEventTypeMap = new Map([...accessibilityEventMap].map(([k, v]) => [v, k]));
|
||||
}
|
||||
|
||||
let accessibilityStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener;
|
||||
let touchExplorationStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
|
||||
|
||||
function updateAccessibilityServiceState() {
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
if (!accessibilityManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
accessibilityServiceEnabled = !!accessibilityManager.isEnabled() && !!accessibilityManager.isTouchExplorationEnabled();
|
||||
}
|
||||
|
||||
let accessibilityServiceEnabled: boolean;
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
if (typeof accessibilityServiceEnabled === 'boolean') {
|
||||
return accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
accessibilityStateChangeListener = new androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener({
|
||||
onAccessibilityStateChanged(enabled) {
|
||||
updateAccessibilityServiceState();
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`AccessibilityStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
touchExplorationStateChangeListener = new androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener({
|
||||
onTouchExplorationStateChanged(enabled) {
|
||||
updateAccessibilityServiceState();
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`TouchExplorationStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
androidx.core.view.accessibility.AccessibilityManagerCompat.addAccessibilityStateChangeListener(accessibilityManager, accessibilityStateChangeListener);
|
||||
androidx.core.view.accessibility.AccessibilityManagerCompat.addTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
|
||||
|
||||
updateAccessibilityServiceState();
|
||||
|
||||
Application.on(Application.exitEvent, (args: Application.ApplicationEventData) => {
|
||||
const activity = args.android as android.app.Activity;
|
||||
if (activity && !activity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
if (accessibilityManager) {
|
||||
if (accessibilityStateChangeListener) {
|
||||
androidx.core.view.accessibility.AccessibilityManagerCompat.removeAccessibilityStateChangeListener(accessibilityManager, accessibilityStateChangeListener);
|
||||
}
|
||||
|
||||
if (touchExplorationStateChangeListener) {
|
||||
androidx.core.view.accessibility.AccessibilityManagerCompat.removeTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
accessibilityStateChangeListener = null;
|
||||
touchExplorationStateChangeListener = null;
|
||||
|
||||
Application.off(Application.resumeEvent, updateAccessibilityServiceState);
|
||||
});
|
||||
|
||||
Application.on(Application.resumeEvent, updateAccessibilityServiceState);
|
||||
|
||||
return accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
export function setupAccessibleView(view: View): void {
|
||||
updateAccessibilityProperties(view);
|
||||
}
|
||||
|
||||
export function updateAccessibilityProperties(view: View): void {
|
||||
if (!view.nativeViewProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAccessibilityDelegate(view);
|
||||
applyContentDescription(view);
|
||||
}
|
||||
|
||||
export function sendAccessibilityEvent(view: View, eventType: AndroidAccessibilityEvent, text?: string): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cls = `sendAccessibilityEvent(${view}, ${eventType}, ${text})`;
|
||||
|
||||
const androidView = view.nativeViewProtected as android.view.View;
|
||||
if (!androidView) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls}: no nativeView`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventType) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls}: no eventName provided`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - TalkBack not enabled`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const accessibilityManager = getAndroidAccessibilityManager();
|
||||
if (!accessibilityManager?.isEnabled()) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - accessibility service not enabled`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!accessibilityEventMap.has(eventType)) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - unknown event`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const eventInt = accessibilityEventMap.get(eventType);
|
||||
if (!text) {
|
||||
return androidView.sendAccessibilityEvent(eventInt);
|
||||
}
|
||||
|
||||
const accessibilityEvent = android.view.accessibility.AccessibilityEvent.obtain(eventInt);
|
||||
accessibilityEvent.setSource(androidView);
|
||||
|
||||
accessibilityEvent.getText().clear();
|
||||
|
||||
if (!text) {
|
||||
applyContentDescription(view);
|
||||
|
||||
text = androidView.getContentDescription() || view['title'];
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - text not provided use androidView.getContentDescription() - ${text}`, Trace.categories.Accessibility);
|
||||
}
|
||||
}
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls}: send event with text: '${JSON.stringify(text)}'`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
if (text) {
|
||||
accessibilityEvent.getText().add(text);
|
||||
}
|
||||
|
||||
accessibilityManager.sendAccessibilityEvent(accessibilityEvent);
|
||||
}
|
||||
|
||||
export function updateContentDescription(view: View, forceUpdate?: boolean): string | null {
|
||||
if (!view.nativeViewProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
return applyContentDescription(view, forceUpdate);
|
||||
}
|
||||
|
||||
function setAccessibilityDelegate(view: View): void {
|
||||
if (!view.nativeViewProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureNativeClasses();
|
||||
|
||||
const androidView = view.nativeViewProtected as android.view.View;
|
||||
if (!androidView) {
|
||||
return;
|
||||
}
|
||||
|
||||
androidViewToTNSView.set(androidView, new WeakRef(view));
|
||||
|
||||
const hasOldDelegate = androidView.getAccessibilityDelegate() === TNSAccessibilityDelegate;
|
||||
|
||||
if (hasOldDelegate) {
|
||||
return;
|
||||
}
|
||||
|
||||
androidView.setAccessibilityDelegate(TNSAccessibilityDelegate);
|
||||
}
|
||||
|
||||
function applyContentDescription(view: View, forceUpdate?: boolean) {
|
||||
let androidView = view.nativeViewProtected as android.view.View;
|
||||
if (!androidView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (androidView instanceof androidx.appcompat.widget.Toolbar) {
|
||||
const numChildren = androidView.getChildCount();
|
||||
|
||||
for (let i = 0; i < numChildren; i += 1) {
|
||||
const childAndroidView = androidView.getChildAt(i);
|
||||
if (childAndroidView instanceof androidx.appcompat.widget.AppCompatTextView) {
|
||||
androidView = childAndroidView;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cls = `applyContentDescription(${view})`;
|
||||
if (!androidView) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const titleValue = view['title'] as string;
|
||||
const textValue = view['text'] as string;
|
||||
|
||||
if (!forceUpdate && view._androidContentDescriptionUpdated === false && textValue === view['_lastText'] && titleValue === view['_lastTitle']) {
|
||||
// prevent updating this too much
|
||||
return androidView.getContentDescription();
|
||||
}
|
||||
|
||||
const contentDescriptionBuilder = new Array<string>();
|
||||
|
||||
// Workaround: TalkBack won't read the checked state for fake Switch.
|
||||
if (view.accessibilityRole === AccessibilityRole.Switch) {
|
||||
const androidSwitch = new android.widget.Switch(Application.android.context);
|
||||
if (view.accessibilityState === AccessibilityState.Checked) {
|
||||
contentDescriptionBuilder.push(androidSwitch.getTextOn());
|
||||
} else {
|
||||
contentDescriptionBuilder.push(androidSwitch.getTextOff());
|
||||
}
|
||||
}
|
||||
|
||||
if (view.accessibilityLabel) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - have accessibilityLabel`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${view.accessibilityLabel}`);
|
||||
}
|
||||
|
||||
if (view.accessibilityValue) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - have accessibilityValue`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${view.accessibilityValue}`);
|
||||
} else if (textValue) {
|
||||
if (textValue !== view.accessibilityLabel) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - don't have accessibilityValue - use 'text' value`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${textValue}`);
|
||||
}
|
||||
} else if (titleValue) {
|
||||
if (titleValue !== view.accessibilityLabel) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - don't have accessibilityValue - use 'title' value`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${titleValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (view.accessibilityHint) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - have accessibilityHint`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
contentDescriptionBuilder.push(`${view.accessibilityHint}`);
|
||||
}
|
||||
|
||||
const contentDescription = contentDescriptionBuilder.join('. ').trim().replace(/^\.$/, '');
|
||||
|
||||
if (contentDescription) {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - set to "${contentDescription}"`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
androidView.setContentDescription(contentDescription);
|
||||
} else {
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`${cls} - remove value`, Trace.categories.Accessibility);
|
||||
}
|
||||
|
||||
androidView.setContentDescription(null);
|
||||
}
|
||||
|
||||
view['_lastTitle'] = titleValue;
|
||||
view['_lastText'] = textValue;
|
||||
view._androidContentDescriptionUpdated = false;
|
||||
|
||||
return contentDescription;
|
||||
}
|
36
packages/core/accessibility/index.d.ts
vendored
Normal file
36
packages/core/accessibility/index.d.ts
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
import { View } from '../ui/core/view';
|
||||
import { AndroidAccessibilityEvent } from './accessibility-types';
|
||||
|
||||
export * from './accessibility-common';
|
||||
export * from './accessibility-types';
|
||||
export * from './font-scale';
|
||||
|
||||
/**
|
||||
* Initialize accessibility for View. This should be called on loaded-event.
|
||||
*/
|
||||
export function setupAccessibleView(view: View): void;
|
||||
|
||||
/**
|
||||
* Update accessibility properties on nativeView
|
||||
*/
|
||||
export function updateAccessibilityProperties(view: View): void;
|
||||
|
||||
/**
|
||||
* Android helper function for triggering accessibility events
|
||||
*/
|
||||
export function sendAccessibilityEvent(View: View, eventName: AndroidAccessibilityEvent, text?: string): void;
|
||||
|
||||
/**
|
||||
* Update the content description for android views
|
||||
*/
|
||||
export function updateContentDescription(View: View, forceUpdate?: boolean): string | null;
|
||||
|
||||
/**
|
||||
* Is Android TalkBack or iOS VoiceOver enabled?
|
||||
*/
|
||||
export function isAccessibilityServiceEnabled(): boolean;
|
||||
|
||||
/**
|
||||
* Find the last view focused on a page.
|
||||
*/
|
||||
export function getLastFocusedViewOnPage(page: Page): View | null;
|
264
packages/core/accessibility/index.ios.ts
Normal file
264
packages/core/accessibility/index.ios.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import * as Application from '../application';
|
||||
import type { View } from '../ui/core/view';
|
||||
import { notifyAccessibilityFocusState } from './accessibility-common';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-types';
|
||||
|
||||
export * from './accessibility-common';
|
||||
export * from './accessibility-types';
|
||||
export * from './font-scale';
|
||||
|
||||
function enforceArray(val: string | string[]): string[] {
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
|
||||
if (typeof val === 'string') {
|
||||
return val.split(/[, ]/g).filter((v: string) => !!v);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array of values into a bitmask.
|
||||
*
|
||||
* @param values string values
|
||||
* @param map map lower-case name to integer value.
|
||||
*/
|
||||
function inputArrayToBitMask(values: string | string[], map: Map<string, number>): number {
|
||||
return (
|
||||
enforceArray(values)
|
||||
.filter((value) => !!value)
|
||||
.map((value) => `${value}`.toLocaleLowerCase())
|
||||
.filter((value) => map.has(value))
|
||||
.reduce((res, value) => res | map.get(value), 0) || 0
|
||||
);
|
||||
}
|
||||
|
||||
let AccessibilityTraitsMap: Map<string, number>;
|
||||
let RoleTypeMap: Map<AccessibilityRole, number>;
|
||||
|
||||
let nativeFocusedNotificationObserver;
|
||||
let lastFocusedView: WeakRef<View>;
|
||||
function ensureNativeClasses() {
|
||||
if (AccessibilityTraitsMap && nativeFocusedNotificationObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
AccessibilityTraitsMap = new Map<AccessibilityTrait, number>([
|
||||
[AccessibilityTrait.None, UIAccessibilityTraitNone],
|
||||
[AccessibilityTrait.Button, UIAccessibilityTraitButton],
|
||||
[AccessibilityTrait.Link, UIAccessibilityTraitLink],
|
||||
[AccessibilityTrait.SearchField, UIAccessibilityTraitSearchField],
|
||||
[AccessibilityTrait.Image, UIAccessibilityTraitImage],
|
||||
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
|
||||
[AccessibilityTrait.PlaysSound, UIAccessibilityTraitPlaysSound],
|
||||
[AccessibilityTrait.StaticText, UIAccessibilityTraitStaticText],
|
||||
[AccessibilityTrait.SummaryElement, UIAccessibilityTraitSummaryElement],
|
||||
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
|
||||
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
|
||||
[AccessibilityTrait.StartsMediaSession, UIAccessibilityTraitStartsMediaSession],
|
||||
[AccessibilityTrait.Adjustable, UIAccessibilityTraitAdjustable],
|
||||
[AccessibilityTrait.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction],
|
||||
[AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn],
|
||||
[AccessibilityTrait.Header, UIAccessibilityTraitHeader],
|
||||
]);
|
||||
|
||||
RoleTypeMap = new Map<AccessibilityRole, number>([
|
||||
[AccessibilityRole.Button, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Header, UIAccessibilityTraitHeader],
|
||||
[AccessibilityRole.Link, UIAccessibilityTraitLink],
|
||||
[AccessibilityRole.Search, UIAccessibilityTraitSearchField],
|
||||
[AccessibilityRole.Image, UIAccessibilityTraitImage],
|
||||
[AccessibilityRole.ImageButton, UIAccessibilityTraitImage | UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
|
||||
[AccessibilityRole.StaticText, UIAccessibilityTraitStaticText],
|
||||
[AccessibilityRole.Summary, UIAccessibilityTraitSummaryElement],
|
||||
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
|
||||
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Switch, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.RadioButton, UIAccessibilityTraitButton],
|
||||
]);
|
||||
|
||||
nativeFocusedNotificationObserver = Application.ios.addNotificationObserver(UIAccessibilityElementFocusedNotification, (args: NSNotification) => {
|
||||
const uiView = args.userInfo.objectForKey(UIAccessibilityFocusedElementKey) as UIView;
|
||||
if (!uiView.tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootView = Application.getRootView();
|
||||
|
||||
// We use the UIView's tag to find the NativeScript View by its domId.
|
||||
let view = rootView.getViewByDomId<View>(uiView.tag);
|
||||
if (!view) {
|
||||
for (const modalView of <Array<View>>rootView._getRootModalViews()) {
|
||||
view = modalView.getViewByDomId(uiView.tag);
|
||||
if (view) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastView = lastFocusedView?.get();
|
||||
if (lastView && view !== lastView) {
|
||||
const lastFocusedUIView = lastView.nativeViewProtected as UIView;
|
||||
if (lastFocusedUIView) {
|
||||
lastFocusedView = null;
|
||||
|
||||
notifyAccessibilityFocusState(lastView, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
lastFocusedView = new WeakRef(view);
|
||||
|
||||
notifyAccessibilityFocusState(view, true, false);
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeFocusedNotificationObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeFocusedNotificationObserver, UIAccessibilityElementFocusedNotification);
|
||||
}
|
||||
|
||||
nativeFocusedNotificationObserver = null;
|
||||
lastFocusedView = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function setupAccessibleView(view: View): void {
|
||||
const uiView = view.nativeViewProtected as UIView;
|
||||
if (!uiView) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to map back from the UIView to the NativeScript View.
|
||||
*
|
||||
* We do that by setting the uiView's tag to the View's domId.
|
||||
* This way we can do reverse lookup.
|
||||
*/
|
||||
uiView.tag = view._domId;
|
||||
}
|
||||
|
||||
export function updateAccessibilityProperties(view: View): void {
|
||||
const uiView = view.nativeViewProtected as UIView;
|
||||
if (!uiView) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureNativeClasses();
|
||||
|
||||
const accessibilityRole = view.accessibilityRole;
|
||||
const accessibilityState = view.accessibilityState;
|
||||
|
||||
if (!view.accessible || view.accessibilityHidden) {
|
||||
uiView.accessibilityTraits = UIAccessibilityTraitNone;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let a11yTraits = UIAccessibilityTraitNone;
|
||||
if (RoleTypeMap.has(accessibilityRole)) {
|
||||
a11yTraits |= RoleTypeMap.get(accessibilityRole);
|
||||
}
|
||||
|
||||
switch (accessibilityRole) {
|
||||
case AccessibilityRole.Checkbox:
|
||||
case AccessibilityRole.RadioButton:
|
||||
case AccessibilityRole.Switch: {
|
||||
if (accessibilityState === AccessibilityState.Checked) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.Selected);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (accessibilityState === AccessibilityState.Selected) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.Selected);
|
||||
}
|
||||
if (accessibilityState === AccessibilityState.Disabled) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.NotEnabled);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const UpdatesFrequentlyTrait = AccessibilityTraitsMap.get(AccessibilityTrait.UpdatesFrequently);
|
||||
|
||||
switch (view.accessibilityLiveRegion) {
|
||||
case AccessibilityLiveRegion.Polite:
|
||||
case AccessibilityLiveRegion.Assertive: {
|
||||
a11yTraits |= UpdatesFrequentlyTrait;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
a11yTraits &= ~UpdatesFrequentlyTrait;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (view.accessibilityMediaSession) {
|
||||
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.StartsMediaSession);
|
||||
}
|
||||
|
||||
if (view.accessibilityTraits) {
|
||||
a11yTraits |= inputArrayToBitMask(view.accessibilityTraits, AccessibilityTraitsMap);
|
||||
}
|
||||
|
||||
uiView.accessibilityTraits = a11yTraits;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
export const sendAccessibilityEvent = (): void => {};
|
||||
export const updateContentDescription = (): string | null => null;
|
||||
|
||||
let accessibilityServiceEnabled: boolean;
|
||||
let nativeObserver;
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
if (typeof accessibilityServiceEnabled === 'boolean') {
|
||||
return accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
let isVoiceOverRunning: () => boolean;
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning === 'function') {
|
||||
isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning;
|
||||
} else {
|
||||
// iOS is too old to tell us if voice over is enabled
|
||||
if (typeof UIAccessibilityIsVoiceOverRunning !== 'function') {
|
||||
accessibilityServiceEnabled = false;
|
||||
return accessibilityServiceEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
accessibilityServiceEnabled = isVoiceOverRunning();
|
||||
|
||||
let voiceOverStatusChangedNotificationName: string | null = null;
|
||||
if (typeof UIAccessibilityVoiceOverStatusDidChangeNotification !== 'undefined') {
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusDidChangeNotification;
|
||||
} else if (typeof UIAccessibilityVoiceOverStatusChanged !== 'undefined') {
|
||||
voiceOverStatusChangedNotificationName = UIAccessibilityVoiceOverStatusChanged;
|
||||
}
|
||||
|
||||
if (voiceOverStatusChangedNotificationName) {
|
||||
nativeObserver = Application.ios.addNotificationObserver(voiceOverStatusChangedNotificationName, () => {
|
||||
accessibilityServiceEnabled = isVoiceOverRunning();
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeObserver, voiceOverStatusChangedNotificationName);
|
||||
}
|
||||
|
||||
accessibilityServiceEnabled = undefined;
|
||||
nativeObserver = null;
|
||||
});
|
||||
}
|
||||
|
||||
Application.on(Application.resumeEvent, () => {
|
||||
accessibilityServiceEnabled = isVoiceOverRunning();
|
||||
});
|
||||
|
||||
return accessibilityServiceEnabled;
|
||||
}
|
@ -28,6 +28,7 @@ export const uncaughtErrorEvent = 'uncaughtError';
|
||||
export const discardedErrorEvent = 'discardedError';
|
||||
export const orientationChangedEvent = 'orientationChanged';
|
||||
export const systemAppearanceChangedEvent = 'systemAppearanceChanged';
|
||||
export const fontScaleChangedEvent = 'fontScaleChanged';
|
||||
|
||||
const ORIENTATION_CSS_CLASSES = [`${CSSUtils.CLASS_PREFIX}${Enums.DeviceOrientation.portrait}`, `${CSSUtils.CLASS_PREFIX}${Enums.DeviceOrientation.landscape}`, `${CSSUtils.CLASS_PREFIX}${Enums.DeviceOrientation.unknown}`];
|
||||
|
||||
@ -122,7 +123,7 @@ function increaseStyleScopeApplicationCssSelectorVersion(rootView: View) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyCssClass(rootView: View, cssClasses: string[], newCssClass: string) {
|
||||
export function applyCssClass(rootView: View, cssClasses: string[], newCssClass: string): void {
|
||||
if (!rootView.cssClasses.has(newCssClass)) {
|
||||
cssClasses.forEach((cssClass) => removeCssClass(rootView, cssClass));
|
||||
addCssClass(rootView, newCssClass);
|
||||
@ -146,7 +147,7 @@ export function orientationChanged(rootView: View, newOrientation: 'portrait' |
|
||||
}
|
||||
export let autoSystemAppearanceChanged = true;
|
||||
|
||||
export function setAutoSystemAppearanceChanged(value: boolean) {
|
||||
export function setAutoSystemAppearanceChanged(value: boolean): void {
|
||||
autoSystemAppearanceChanged = value;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ import { NavigationEntry, AndroidActivityCallbacks } from '../ui/frame/frame-int
|
||||
import { Observable } from '../data/observable';
|
||||
|
||||
import { profile } from '../profiling';
|
||||
import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-helper';
|
||||
import { initAccessibilityFontScale } from '../accessibility/font-scale';
|
||||
|
||||
const ActivityCreated = 'activityCreated';
|
||||
const ActivityDestroyed = 'activityDestroyed';
|
||||
@ -172,6 +174,9 @@ export function run(entry?: NavigationEntry | string) {
|
||||
const nativeApp = getNativeApplication();
|
||||
androidApp.init(nativeApp);
|
||||
}
|
||||
|
||||
initAccessibilityCssHelper();
|
||||
initAccessibilityFontScale();
|
||||
}
|
||||
|
||||
export function addCss(cssText: string, attributeScoped?: boolean): void {
|
||||
|
12
packages/core/application/index.d.ts
vendored
12
packages/core/application/index.d.ts
vendored
@ -54,6 +54,11 @@ export const orientationChangedEvent: string;
|
||||
*/
|
||||
export const systemAppearanceChangedEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to fontScaleChanged event.
|
||||
*/
|
||||
export const fontScaleChangedEvent: string;
|
||||
|
||||
/**
|
||||
* Boolean to enable/disable systemAppearanceChanged
|
||||
*/
|
||||
@ -62,7 +67,7 @@ export let autoSystemAppearanceChanged: boolean;
|
||||
/**
|
||||
* enable/disable systemAppearanceChanged
|
||||
*/
|
||||
export function setAutoSystemAppearanceChanged(value: boolean);
|
||||
export function setAutoSystemAppearanceChanged(value: boolean): void;
|
||||
|
||||
/**
|
||||
* Updates root view classes including those of modals
|
||||
@ -184,6 +189,11 @@ export function setCssFileName(cssFile: string): void;
|
||||
*/
|
||||
export function getCssFileName(): string;
|
||||
|
||||
/**
|
||||
* Ensure css-class is set on rootView
|
||||
*/
|
||||
export function applyCssClass(rootView: View, cssClasses: string[], newCssClass: string): void;
|
||||
|
||||
/**
|
||||
* Loads immediately the app.css.
|
||||
* By default the app.css file is loaded shortly after "loaded".
|
||||
|
@ -18,6 +18,8 @@ import { IOSHelper } from '../ui/core/view/view-helper';
|
||||
import { Device } from '../platform';
|
||||
import { profile } from '../profiling';
|
||||
import { iOSNativeHelper } from '../utils';
|
||||
import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-helper';
|
||||
import { initAccessibilityFontScale } from '../accessibility/font-scale';
|
||||
|
||||
const IOS_PLATFORM = 'ios';
|
||||
|
||||
@ -435,6 +437,9 @@ export function run(entry?: string | NavigationEntry) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initAccessibilityCssHelper();
|
||||
initAccessibilityFontScale();
|
||||
}
|
||||
|
||||
export function addCss(cssText: string, attributeScoped?: boolean): void {
|
||||
|
@ -185,6 +185,7 @@ export namespace Trace {
|
||||
* all predefined categories.
|
||||
*/
|
||||
export namespace categories {
|
||||
export const Accessibility = 'Accessibility';
|
||||
export const VisualTreeEvents = 'VisualTreeEvents';
|
||||
export const Layout = 'Layout';
|
||||
export const Style = 'Style';
|
||||
|
@ -6,6 +6,7 @@ import { layout, RESOURCE_PREFIX, isFontIconURI } from '../../utils';
|
||||
import { colorProperty } from '../styling/style-properties';
|
||||
import { ImageSource } from '../../image-source';
|
||||
import * as application from '../../application';
|
||||
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../accessibility';
|
||||
|
||||
export * from './action-bar-common';
|
||||
|
||||
@ -298,7 +299,7 @@ export class ActionBar extends ActionBarBase {
|
||||
}
|
||||
}
|
||||
|
||||
public _updateTitleAndTitleView() {
|
||||
public _updateTitleAndTitleView(): void {
|
||||
if (!this.titleView) {
|
||||
// No title view - show the title
|
||||
const title = this.title;
|
||||
@ -313,6 +314,9 @@ export class ActionBar extends ActionBarBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update content description for the screen reader.
|
||||
updateContentDescription(this, true);
|
||||
}
|
||||
|
||||
public _addActionItems() {
|
||||
@ -447,6 +451,74 @@ export class ActionBar extends ActionBarBase {
|
||||
this.nativeViewProtected.setContentInsetsAbsolute(this.effectiveContentInsetLeft, this.effectiveContentInsetRight);
|
||||
}
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeView = this.nativeViewProtected;
|
||||
if (!nativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFocusableState = android.os.Build.VERSION.SDK_INT >= 26 && nativeView.getFocusable();
|
||||
const originalImportantForAccessibility = nativeView.getImportantForAccessibility();
|
||||
const originalIsAccessibilityHeading = android.os.Build.VERSION.SDK_INT >= 28 && nativeView.isAccessibilityHeading();
|
||||
|
||||
try {
|
||||
nativeView.setFocusable(false);
|
||||
nativeView.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
let announceView: android.view.View | null = null;
|
||||
|
||||
const numChildren = nativeView.getChildCount();
|
||||
for (let i = 0; i < numChildren; i += 1) {
|
||||
const childView = nativeView.getChildAt(i);
|
||||
if (!childView) {
|
||||
continue;
|
||||
}
|
||||
|
||||
childView.setFocusable(true);
|
||||
if (childView instanceof androidx.appcompat.widget.AppCompatTextView) {
|
||||
announceView = childView;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
announceView.setAccessibilityHeading(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!announceView) {
|
||||
announceView = nativeView;
|
||||
}
|
||||
|
||||
announceView.setFocusable(true);
|
||||
announceView.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
// Reset status after the focus have been reset.
|
||||
const localNativeView = this.nativeViewProtected;
|
||||
if (!localNativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
nativeView.setAccessibilityHeading(originalIsAccessibilityHeading);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 26) {
|
||||
localNativeView.setFocusable(originalFocusableState);
|
||||
}
|
||||
|
||||
localNativeView.setImportantForAccessibility(originalImportantForAccessibility);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAppCompatTextView(toolbar: androidx.appcompat.widget.Toolbar): typeof AppCompatTextView {
|
||||
|
@ -5,6 +5,7 @@ import { Color } from '../../color';
|
||||
import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties';
|
||||
import { ImageSource } from '../../image-source';
|
||||
import { layout, iOSNativeHelper, isFontIconURI } from '../../utils';
|
||||
import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties';
|
||||
|
||||
export * from './action-bar-common';
|
||||
|
||||
@ -99,6 +100,46 @@ export class ActionBar extends ActionBarBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
[accessibilityValueProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityValue = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityLabelProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLabel = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityLabel = value;
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityHintProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityHint = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityHint = value;
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityLanguageProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLanguage = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityLanguage = value;
|
||||
}
|
||||
}
|
||||
|
||||
public createNativeView(): UIView {
|
||||
return this.ios;
|
||||
}
|
||||
@ -148,7 +189,18 @@ export class ActionBar extends ActionBarBase {
|
||||
}
|
||||
}
|
||||
|
||||
public update() {
|
||||
private _getNavigationItem(): UINavigationItem | null {
|
||||
const page = this.page;
|
||||
// Page should be attached to frame to update the action bar.
|
||||
if (!page || !page.frame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewController = <UIViewController>page.ios;
|
||||
return viewController.navigationItem;
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
const page = this.page;
|
||||
// Page should be attached to frame to update the action bar.
|
||||
if (!page || !page.frame) {
|
||||
@ -228,6 +280,12 @@ export class ActionBar extends ActionBarBase {
|
||||
if (!this.isLayoutValid) {
|
||||
this.layoutInternal();
|
||||
}
|
||||
|
||||
// Make sure accessibility values are up-to-date on the navigationItem
|
||||
navigationItem.accessibilityValue = this.accessibilityValue;
|
||||
navigationItem.accessibilityLabel = this.accessibilityLabel;
|
||||
navigationItem.accessibilityLanguage = this.accessibilityLanguage;
|
||||
navigationItem.accessibilityHint = this.accessibilityHint;
|
||||
}
|
||||
|
||||
private populateMenuItems(navigationItem: UINavigationItem) {
|
||||
|
@ -2,11 +2,15 @@ import { Button as ButtonDefinition } from '.';
|
||||
import { TextBase } from '../text-base';
|
||||
import { CSSType } from '../core/view';
|
||||
import { booleanConverter } from '../core/view-base';
|
||||
import { AccessibilityRole } from '../../accessibility';
|
||||
|
||||
@CSSType('Button')
|
||||
export abstract class ButtonBase extends TextBase implements ButtonDefinition {
|
||||
public static tapEvent = 'tap';
|
||||
|
||||
accessible = true;
|
||||
accessibilityRole = AccessibilityRole.Button;
|
||||
|
||||
get textWrap(): boolean {
|
||||
return this.style.whiteSpace === 'normal';
|
||||
}
|
||||
|
13
packages/core/ui/core/view-base/index.d.ts
vendored
13
packages/core/ui/core/view-base/index.d.ts
vendored
@ -34,6 +34,14 @@ export function isEventOrGesture(name: string, view: ViewBase): boolean;
|
||||
*/
|
||||
export function getViewById(view: ViewBase, id: string): ViewBase;
|
||||
|
||||
/**
|
||||
* Gets a child view by domId.
|
||||
* @param view - The parent (container) view of the view to look for.
|
||||
* @param domId - The id of the view to look for.
|
||||
* Returns an instance of a view (if found), otherwise undefined.
|
||||
*/
|
||||
export function getViewByDomId(view: ViewBase, domId: number): ViewBase;
|
||||
|
||||
export interface ShowModalOptions {
|
||||
/**
|
||||
* Any context you want to pass to the modally shown view. This same context will be available in the arguments of the shownModally event handler.
|
||||
@ -289,6 +297,11 @@ export abstract class ViewBase extends Observable {
|
||||
*/
|
||||
public getViewById<T extends ViewBase>(id: string): T;
|
||||
|
||||
/**
|
||||
* Returns the child view with the specified domId.
|
||||
*/
|
||||
public getViewByDomId<T extends ViewBase>(id: number): T;
|
||||
|
||||
/**
|
||||
* Load view.
|
||||
* @param view to load.
|
||||
|
@ -143,6 +143,32 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBaseDefinition {
|
||||
if (!view) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (view._domId === domId) {
|
||||
return view;
|
||||
}
|
||||
|
||||
let retVal: ViewBaseDefinition;
|
||||
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
|
||||
if (view._domId === domId) {
|
||||
retVal = child;
|
||||
|
||||
// break the iteration by returning false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
eachDescendant(view, descendantsCallback);
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
|
||||
if (!callback || !view) {
|
||||
return;
|
||||
@ -373,6 +399,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
return <T>getViewById(this, id);
|
||||
}
|
||||
|
||||
getViewByDomId<T extends ViewBaseDefinition>(domId: number): T {
|
||||
return <T>getViewByDomId(this, domId);
|
||||
}
|
||||
|
||||
get page(): Page {
|
||||
if (this.parent) {
|
||||
return this.parent.page;
|
||||
|
@ -1,8 +1,9 @@
|
||||
// Definitions.
|
||||
import { Point, CustomLayoutView as CustomLayoutViewDefinition, dip } from '.';
|
||||
import { GestureTypes, GestureEventData } from '../../gestures';
|
||||
import type { Point, CustomLayoutView as CustomLayoutViewDefinition, dip } from '.';
|
||||
import type { GestureTypes, GestureEventData } from '../../gestures';
|
||||
|
||||
// Types.
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty } from '../../styling/style-properties';
|
||||
import { layout } from '../../../utils';
|
||||
import { Trace } from '../../../trace';
|
||||
@ -48,6 +49,9 @@ import { Screen } from '../../../platform';
|
||||
import { AndroidActivityBackPressedEventData, android as androidApp } from '../../../application';
|
||||
import { Device } from '../../../platform';
|
||||
import lazy from '../../../utils/lazy';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, setupAccessibleView, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription } from '../../../accessibility';
|
||||
import * as Utils from '../../../utils';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -323,6 +327,12 @@ export class View extends ViewCommon {
|
||||
|
||||
nativeViewProtected: android.view.View;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.on(View.loadedEvent, () => setupAccessibleView(this));
|
||||
}
|
||||
|
||||
// TODO: Implement unobserve that detach the touchListener.
|
||||
_observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void {
|
||||
super._observe(type, callback, thisArg);
|
||||
@ -744,13 +754,6 @@ export class View extends ViewCommon {
|
||||
org.nativescript.widgets.OriginPoint.setY(this.nativeViewProtected, value);
|
||||
}
|
||||
|
||||
[automationTextProperty.getDefault](): string {
|
||||
return this.nativeViewProtected.getContentDescription();
|
||||
}
|
||||
[automationTextProperty.setNative](value: string) {
|
||||
this.nativeViewProtected.setContentDescription(value);
|
||||
}
|
||||
|
||||
[isUserInteractionEnabledProperty.setNative](value: boolean) {
|
||||
this.nativeViewProtected.setClickable(value);
|
||||
this.nativeViewProtected.setFocusable(value);
|
||||
@ -792,6 +795,77 @@ export class View extends ViewCommon {
|
||||
this.nativeViewProtected.setAlpha(float(value));
|
||||
}
|
||||
|
||||
[accessibilityEnabledProperty.setNative](value: boolean): void {
|
||||
this.nativeViewProtected.setFocusable(!!value);
|
||||
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityIdentifierProperty.setNative](value: string): void {
|
||||
const id = Utils.ad.resources.getId(':id/nativescript_accessibility_id');
|
||||
|
||||
if (id) {
|
||||
this.nativeViewProtected.setTag(id, value);
|
||||
this.nativeViewProtected.setTag(value);
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityRoleProperty.setNative](value: AccessibilityRole): void {
|
||||
updateAccessibilityProperties(this);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
this.nativeViewProtected?.setAccessibilityHeading(value === AccessibilityRole.Header);
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityValueProperty.setNative](): void {
|
||||
this._androidContentDescriptionUpdated = true;
|
||||
updateContentDescription(this);
|
||||
}
|
||||
|
||||
[accessibilityLabelProperty.setNative](): void {
|
||||
this._androidContentDescriptionUpdated = true;
|
||||
updateContentDescription(this);
|
||||
}
|
||||
|
||||
[accessibilityHintProperty.setNative](): void {
|
||||
this._androidContentDescriptionUpdated = true;
|
||||
updateContentDescription(this);
|
||||
}
|
||||
|
||||
[accessibilityHiddenProperty.setNative](value: boolean): void {
|
||||
if (value) {
|
||||
this.nativeViewProtected.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
} else {
|
||||
this.nativeViewProtected.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityLiveRegionProperty.setNative](value: AccessibilityLiveRegion): void {
|
||||
switch (value) {
|
||||
case AccessibilityLiveRegion.Assertive: {
|
||||
this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE);
|
||||
break;
|
||||
}
|
||||
case AccessibilityLiveRegion.Polite: {
|
||||
this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_NONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityStateProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityMediaSessionProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[androidElevationProperty.getDefault](): number {
|
||||
return this.getDefaultElevation();
|
||||
}
|
||||
@ -1047,6 +1121,30 @@ export class View extends ViewCommon {
|
||||
(<any>nativeView).background = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAccessibilityEvent(this, eventName, msg);
|
||||
}
|
||||
|
||||
public accessibilityAnnouncement(msg = this.accessibilityLabel): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.androidSendAccessibilityEvent(AndroidAccessibilityEvent.ANNOUNCEMENT, msg);
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.androidSendAccessibilityEvent(AndroidAccessibilityEvent.WINDOW_STATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
export class ContainerView extends View {
|
||||
|
109
packages/core/ui/core/view/index.d.ts
vendored
109
packages/core/ui/core/view/index.d.ts
vendored
@ -4,8 +4,9 @@ import { EventData } from '../../../data/observable';
|
||||
import { Color } from '../../../color';
|
||||
import { Animation, AnimationDefinition, AnimationPromise } from '../../animation';
|
||||
import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from '../../styling/style-properties';
|
||||
import { GestureTypes, GestureEventData, GesturesObserver } from '../../gestures';
|
||||
import { GestureTypes, GesturesObserver } from '../../gestures';
|
||||
import { LinearGradient } from '../../styling/gradient';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AndroidAccessibilityEvent, IOSPostAccessibilityNotificationType } from '../../../accessibility/accessibility-types';
|
||||
import { BoxShadow } from '../../styling/box-shadow';
|
||||
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -131,6 +132,21 @@ export abstract class View extends ViewBase {
|
||||
*/
|
||||
public static shownModallyEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to accessibilityBlur event.
|
||||
*/
|
||||
public static accessibilityBlurEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to accessibilityFocus event.
|
||||
*/
|
||||
public static accessibilityFocusEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to accessibilityFocusChanged event.
|
||||
*/
|
||||
public static accessibilityFocusChangedEvent: string;
|
||||
|
||||
/**
|
||||
* Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform.
|
||||
*/
|
||||
@ -226,6 +242,68 @@ export abstract class View extends ViewBase {
|
||||
*/
|
||||
color: Color;
|
||||
|
||||
/**
|
||||
* If `true` the element is an accessibility element and all the children will be treated as a single selectable component.
|
||||
*/
|
||||
accessible: boolean;
|
||||
|
||||
/**
|
||||
* Hide the view and its children from the a11y service
|
||||
*/
|
||||
accessibilityHidden: boolean;
|
||||
|
||||
/**
|
||||
* The view's unique accessibilityIdentifier.
|
||||
*
|
||||
* This is used for automated testing.
|
||||
*/
|
||||
accessibilityIdentifier: string;
|
||||
|
||||
/**
|
||||
* Which role should this view be treated by the a11y service?
|
||||
*/
|
||||
accessibilityRole: AccessibilityRole;
|
||||
|
||||
/**
|
||||
* Which state should this view be treated as by the a11y service?
|
||||
*/
|
||||
accessibilityState: AccessibilityState;
|
||||
|
||||
/**
|
||||
* Short description of the element, ideally one word.
|
||||
*/
|
||||
accessibilityLabel: string;
|
||||
|
||||
/**
|
||||
* Current value of the element in a localized string.
|
||||
*/
|
||||
accessibilityValue: string;
|
||||
|
||||
/**
|
||||
* A hint describes the elements behavior. Example: 'Tap change playback speed'
|
||||
*/
|
||||
accessibilityHint: string;
|
||||
accessibilityTraits?: AccessibilityTrait[];
|
||||
accessibilityLiveRegion: AccessibilityLiveRegion;
|
||||
|
||||
/**
|
||||
* Sets the language in which to speak the element's label and value.
|
||||
* Accepts language ID tags that follows the "BCP 47" specification.
|
||||
*/
|
||||
accessibilityLanguage: string;
|
||||
|
||||
/**
|
||||
* This view starts a media session. Equivalent to trait = startsMedia
|
||||
*/
|
||||
accessibilityMediaSession: boolean;
|
||||
|
||||
/**
|
||||
* Internal use only. This is used to limit the number of updates to android.view.View.setContentDescription()
|
||||
*/
|
||||
_androidContentDescriptionUpdated?: boolean;
|
||||
|
||||
automationText: string;
|
||||
|
||||
/**
|
||||
* Gets or sets the elevation of the android view.
|
||||
*/
|
||||
@ -364,11 +442,6 @@ export abstract class View extends ViewBase {
|
||||
|
||||
//END Style property shortcuts
|
||||
|
||||
/**
|
||||
* Gets or sets the automation text of the view.
|
||||
*/
|
||||
automationText: string;
|
||||
|
||||
/**
|
||||
* Gets or sets the X component of the origin point around which the view will be transformed. The default value is 0.5 representing the center of the view.
|
||||
*/
|
||||
@ -673,6 +746,29 @@ export abstract class View extends ViewBase {
|
||||
*/
|
||||
public eachChildView(callback: (view: View) => boolean): void;
|
||||
|
||||
/**
|
||||
* Android: Send accessibility event
|
||||
*/
|
||||
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void;
|
||||
|
||||
/**
|
||||
* iOS: post accessibility notification.
|
||||
* type = 'announcement' will announce `args` via VoiceOver. If no args element will be announced instead.
|
||||
* type = 'layout' used when the layout of a screen changes.
|
||||
* type = 'screen' large change made to the screen.
|
||||
*/
|
||||
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void;
|
||||
|
||||
/**
|
||||
* Make an announcement to the screen reader.
|
||||
*/
|
||||
public accessibilityAnnouncement(msg?: string): void;
|
||||
|
||||
/**
|
||||
* Announce screen changed
|
||||
*/
|
||||
public accessibilityScreenChanged(): void;
|
||||
|
||||
//@private
|
||||
/**
|
||||
* @private
|
||||
@ -879,7 +975,6 @@ export interface AddChildFromBuilder {
|
||||
_addChildFromBuilder(name: string, value: any): void;
|
||||
}
|
||||
|
||||
export const automationTextProperty: Property<View, string>;
|
||||
export const originXProperty: Property<View, number>;
|
||||
export const originYProperty: Property<View, number>;
|
||||
export const isEnabledProperty: Property<View, boolean>;
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Point, View as ViewDefinition, dip } from '.';
|
||||
|
||||
// Requires
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { ShowModalOptions } from '../view-base';
|
||||
import { Trace } from '../../../trace';
|
||||
import { layout, iOSNativeHelper } from '../../../utils';
|
||||
@ -10,6 +10,8 @@ import { IOSHelper } from './view-helper';
|
||||
import { ios as iosBackground, Background } from '../../styling/background';
|
||||
import { perspectiveProperty, Visibility, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, clipPathProperty } from '../../styling/style-properties';
|
||||
import { profile } from '../../../profiling';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityTraitsProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { setupAccessibleView, IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties } from '../../../accessibility';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -54,6 +56,12 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
return (this._privateFlags & PFLAG_FORCE_LAYOUT) === PFLAG_FORCE_LAYOUT;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.once(View.loadedEvent, () => setupAccessibleView(this));
|
||||
}
|
||||
|
||||
public requestLayout(): void {
|
||||
super.requestLayout();
|
||||
this._privateFlags |= PFLAG_FORCE_LAYOUT;
|
||||
@ -553,14 +561,65 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
this.updateOriginPoint(this.originX, value);
|
||||
}
|
||||
|
||||
[automationTextProperty.getDefault](): string {
|
||||
[accessibilityEnabledProperty.setNative](value: boolean): void {
|
||||
this.nativeViewProtected.isAccessibilityElement = !!value;
|
||||
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityIdentifierProperty.getDefault](): string {
|
||||
return this.nativeViewProtected.accessibilityLabel;
|
||||
}
|
||||
[automationTextProperty.setNative](value: string) {
|
||||
[accessibilityIdentifierProperty.setNative](value: string): void {
|
||||
this.nativeViewProtected.accessibilityIdentifier = value;
|
||||
}
|
||||
|
||||
[accessibilityRoleProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityTraitsProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityValueProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityValue = value;
|
||||
}
|
||||
|
||||
[accessibilityLabelProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLabel = value;
|
||||
}
|
||||
|
||||
[accessibilityHintProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityHint = value;
|
||||
}
|
||||
|
||||
[accessibilityLanguageProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLanguage = value;
|
||||
}
|
||||
|
||||
[accessibilityHiddenProperty.setNative](value: boolean): void {
|
||||
this.nativeViewProtected.accessibilityElementsHidden = !!value;
|
||||
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityLiveRegionProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityStateProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityMediaSessionProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[isUserInteractionEnabledProperty.getDefault](): boolean {
|
||||
return this.nativeViewProtected.userInteractionEnabled;
|
||||
}
|
||||
@ -673,6 +732,54 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void {
|
||||
if (!notificationType) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notification: number;
|
||||
let args: string | UIView | null = this.nativeViewProtected;
|
||||
if (typeof msg === 'string' && msg) {
|
||||
args = msg;
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case IOSPostAccessibilityNotificationType.Announcement: {
|
||||
notification = UIAccessibilityAnnouncementNotification;
|
||||
break;
|
||||
}
|
||||
case IOSPostAccessibilityNotificationType.Layout: {
|
||||
notification = UIAccessibilityLayoutChangedNotification;
|
||||
break;
|
||||
}
|
||||
case IOSPostAccessibilityNotificationType.Screen: {
|
||||
notification = UIAccessibilityScreenChangedNotification;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UIAccessibilityPostNotification(notification, args ?? null);
|
||||
}
|
||||
|
||||
public accessibilityAnnouncement(msg = this.accessibilityLabel): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iosPostAccessibilityNotification(IOSPostAccessibilityNotificationType.Announcement, msg);
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iosPostAccessibilityNotification(IOSPostAccessibilityNotificationType.Screen);
|
||||
}
|
||||
|
||||
_getCurrentLayoutBounds(): {
|
||||
left: number;
|
||||
top: number;
|
||||
|
@ -22,6 +22,9 @@ import { LinearGradient } from '../../styling/linear-gradient';
|
||||
import { TextTransform } from '../../text-base';
|
||||
|
||||
import * as am from '../../animation';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AndroidAccessibilityEvent, IOSPostAccessibilityNotificationType } from '../../../accessibility/accessibility-types';
|
||||
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityTraitsProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, getCurrentFontScale } from '../../../accessibility';
|
||||
import { BoxShadow } from '../../styling/box-shadow';
|
||||
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -68,6 +71,9 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
public static layoutChangedEvent = 'layoutChanged';
|
||||
public static shownModallyEvent = 'shownModally';
|
||||
public static showingModallyEvent = 'showingModally';
|
||||
public static accessibilityBlurEvent = accessibilityBlurEvent;
|
||||
public static accessibilityFocusEvent = accessibilityFocusEvent;
|
||||
public static accessibilityFocusChangedEvent = accessibilityFocusChangedEvent;
|
||||
|
||||
protected _closeModalCallback: Function;
|
||||
public _manager: any;
|
||||
@ -91,6 +97,8 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
public _gestureObservers = {};
|
||||
|
||||
_androidContentDescriptionUpdated?: boolean;
|
||||
|
||||
get css(): string {
|
||||
const scope = this._styleScope;
|
||||
|
||||
@ -360,6 +368,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
modalRootViewCssClasses.forEach((c) => this.cssClasses.add(c));
|
||||
|
||||
parent._modal = this;
|
||||
this.style._fontScale = getCurrentFontScale();
|
||||
this._modalParent = parent;
|
||||
this._modalContext = options.context;
|
||||
const that = this;
|
||||
@ -743,6 +752,71 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
this.style.scaleY = value;
|
||||
}
|
||||
|
||||
get accessible(): boolean {
|
||||
return this.style.accessible;
|
||||
}
|
||||
set accessible(value: boolean) {
|
||||
this.style.accessible = value;
|
||||
}
|
||||
|
||||
get accessibilityHidden(): boolean {
|
||||
return this.style.accessibilityHidden;
|
||||
}
|
||||
set accessibilityHidden(value: boolean) {
|
||||
this.style.accessibilityHidden = value;
|
||||
}
|
||||
|
||||
public accessibilityIdentifier: string;
|
||||
|
||||
get accessibilityRole(): AccessibilityRole {
|
||||
return this.style.accessibilityRole;
|
||||
}
|
||||
set accessibilityRole(value: AccessibilityRole) {
|
||||
this.style.accessibilityRole = value;
|
||||
}
|
||||
|
||||
get accessibilityState(): AccessibilityState {
|
||||
return this.style.accessibilityState;
|
||||
}
|
||||
set accessibilityState(value: AccessibilityState) {
|
||||
this.style.accessibilityState = value;
|
||||
}
|
||||
|
||||
public accessibilityLabel: string;
|
||||
public accessibilityValue: string;
|
||||
public accessibilityHint: string;
|
||||
|
||||
get accessibilityLiveRegion(): AccessibilityLiveRegion {
|
||||
return this.style.accessibilityLiveRegion;
|
||||
}
|
||||
set accessibilityLiveRegion(value: AccessibilityLiveRegion) {
|
||||
this.style.accessibilityLiveRegion = value;
|
||||
}
|
||||
|
||||
get accessibilityLanguage(): string {
|
||||
return this.style.accessibilityLanguage;
|
||||
}
|
||||
set accessibilityLanguage(value: string) {
|
||||
this.style.accessibilityLanguage = value;
|
||||
}
|
||||
|
||||
get accessibilityMediaSession(): boolean {
|
||||
return this.style.accessibilityMediaSession;
|
||||
}
|
||||
set accessibilityMediaSession(value: boolean) {
|
||||
this.style.accessibilityMediaSession = value;
|
||||
}
|
||||
|
||||
public accessibilityTraits?: AccessibilityTrait[];
|
||||
|
||||
get automationText(): string {
|
||||
return this.accessibilityIdentifier;
|
||||
}
|
||||
|
||||
set automationText(value: string) {
|
||||
this.accessibilityIdentifier = value;
|
||||
}
|
||||
|
||||
get androidElevation(): number {
|
||||
return this.style.androidElevation;
|
||||
}
|
||||
@ -759,7 +833,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
//END Style property shortcuts
|
||||
|
||||
public automationText: string;
|
||||
public originX: number;
|
||||
public originY: number;
|
||||
public isEnabled: boolean;
|
||||
@ -1013,12 +1086,23 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
export const automationTextProperty = new Property<ViewCommon, string>({
|
||||
name: 'automationText',
|
||||
});
|
||||
automationTextProperty.register(ViewCommon);
|
||||
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public accessibilityAnnouncement(msg?: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const originXProperty = new Property<ViewCommon, number>({
|
||||
name: 'originX',
|
||||
@ -1070,3 +1154,9 @@ export const iosIgnoreSafeAreaProperty = new InheritedProperty({
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
iosIgnoreSafeAreaProperty.register(ViewCommon);
|
||||
|
||||
accessibilityIdentifierProperty.register(ViewCommon);
|
||||
accessibilityLabelProperty.register(ViewCommon);
|
||||
accessibilityValueProperty.register(ViewCommon);
|
||||
accessibilityHintProperty.register(ViewCommon);
|
||||
accessibilityTraitsProperty.register(ViewCommon);
|
||||
|
@ -5,6 +5,7 @@ import { ActionBar } from '../action-bar';
|
||||
import { GridLayout } from '../layouts/grid-layout';
|
||||
import { Device } from '../../platform';
|
||||
import { profile } from '../../profiling';
|
||||
import { AndroidAccessibilityEvent, getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
|
||||
|
||||
export * from './page-common';
|
||||
|
||||
@ -122,4 +123,31 @@ export class Page extends PageBase {
|
||||
(<any>window).setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(refocus = false): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refocus) {
|
||||
const lastFocusedView = getLastFocusedViewOnPage(this);
|
||||
if (lastFocusedView) {
|
||||
const announceView = lastFocusedView.nativeViewProtected;
|
||||
if (announceView) {
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionBarHidden || this.accessibilityLabel) {
|
||||
this.androidSendAccessibilityEvent(AndroidAccessibilityEvent.WINDOW_STATE_CHANGED);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionBar.accessibilityScreenChanged();
|
||||
}
|
||||
}
|
||||
|
10
packages/core/ui/page/index.d.ts
vendored
10
packages/core/ui/page/index.d.ts
vendored
@ -93,6 +93,11 @@ export declare class Page extends PageBase {
|
||||
*/
|
||||
public actionBar: ActionBar;
|
||||
|
||||
/**
|
||||
* Should page changed be annnounced to the screen reader.
|
||||
*/
|
||||
public accessibilityAnnouncePageEnabled = true;
|
||||
|
||||
/**
|
||||
* A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
|
||||
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
|
||||
@ -161,6 +166,11 @@ export declare class Page extends PageBase {
|
||||
*/
|
||||
public onNavigatedFrom(isBackNavigation: boolean): void;
|
||||
//@endprivate
|
||||
|
||||
/**
|
||||
* Announce screen changed
|
||||
*/
|
||||
public accessibilityScreenChanged(refocus?: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,7 @@ import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './pag
|
||||
|
||||
import { profile } from '../../profiling';
|
||||
import { iOSNativeHelper, layout } from '../../utils';
|
||||
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
|
||||
|
||||
export * from './page-common';
|
||||
|
||||
@ -522,6 +523,44 @@ export class Page extends PageBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(refocus = false): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refocus) {
|
||||
const lastFocusedView = getLastFocusedViewOnPage(this);
|
||||
if (lastFocusedView) {
|
||||
const uiView = lastFocusedView.nativeViewProtected;
|
||||
if (uiView) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, uiView);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionBarHidden) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.nativeViewProtected);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.accessibilityLabel) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.nativeViewProtected);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.actionBar.accessibilityLabel || this.actionBar.title) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.actionBar.nativeView);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.nativeViewProtected);
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateTopmostController(controller: UIViewController): void {
|
||||
|
@ -33,6 +33,7 @@ export class PageBase extends ContentView {
|
||||
public enableSwipeBackNavigation: boolean;
|
||||
public backgroundSpanUnderStatusBar: boolean;
|
||||
public hasActionBar: boolean;
|
||||
public accessibilityAnnouncePageEnabled = true;
|
||||
|
||||
get navigationContext(): any {
|
||||
return this._navigationContext;
|
||||
@ -126,8 +127,12 @@ export class PageBase extends ContentView {
|
||||
}
|
||||
|
||||
@profile
|
||||
public onNavigatedTo(isBackNavigation: boolean) {
|
||||
public onNavigatedTo(isBackNavigation: boolean): void {
|
||||
this.notify(this.createNavigatedData(PageBase.navigatedToEvent, isBackNavigation));
|
||||
|
||||
if (this.accessibilityAnnouncePageEnabled) {
|
||||
this.accessibilityScreenChanged(!!isBackNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
@profile
|
||||
@ -152,6 +157,10 @@ export class PageBase extends ContentView {
|
||||
get _childrenCount(): number {
|
||||
return (this.content ? 1 : 0) + (this._actionBar ? 1 : 0);
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(refocus?: boolean): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PageBase.prototype.recycleNativeView = 'never';
|
||||
|
24
packages/core/ui/slider/index.d.ts
vendored
24
packages/core/ui/slider/index.d.ts
vendored
@ -1,10 +1,14 @@
|
||||
import { View } from '../core/view';
|
||||
import { Property, CoercibleProperty } from '../core/properties';
|
||||
import { EventData } from '../../data/observable';
|
||||
|
||||
/**
|
||||
* Represents a slider component.
|
||||
*/
|
||||
export class Slider extends View {
|
||||
static readonly accessibilityDecrementEvent = 'accessibilityDecrement';
|
||||
static readonly accessibilityIncrementEvent = 'accessibilityIncrement';
|
||||
|
||||
/**
|
||||
* Gets the native [android widget](http://developer.android.com/reference/android/widget/SeekBar.html) that represents the user interface for this component. Valid only when running on Android OS.
|
||||
*/
|
||||
@ -29,6 +33,11 @@ export class Slider extends View {
|
||||
* Gets or sets a slider max value. The default value is 100.
|
||||
*/
|
||||
maxValue: number;
|
||||
|
||||
/**
|
||||
* Increase/Decrease step size for iOS Increment-/Decrement events
|
||||
*/
|
||||
accessibilityStep: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,3 +54,18 @@ export const minValueProperty: Property<Slider, number>;
|
||||
* Represents the observable property backing the maxValue property of each Slider instance.
|
||||
*/
|
||||
export const maxValueProperty: CoercibleProperty<Slider, number>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the accessibilityStep property of each Slider instance.
|
||||
*/
|
||||
export const accessibilityStepProperty: Property<SliderBase, number>;
|
||||
|
||||
interface AccessibilityIncrementEventData extends EventData {
|
||||
object: Slider;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
interface AccessibilityDecrementEventData extends EventData {
|
||||
object: Slider;
|
||||
value?: number;
|
||||
}
|
||||
|
@ -3,9 +3,44 @@ import { Background } from '../styling/background';
|
||||
import { SliderBase, valueProperty, minValueProperty, maxValueProperty } from './slider-common';
|
||||
import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties';
|
||||
import { Color } from '../../color';
|
||||
import { AccessibilityDecrementEventData, AccessibilityIncrementEventData } from '.';
|
||||
|
||||
export * from './slider-common';
|
||||
|
||||
@NativeClass()
|
||||
class TNSSlider extends UISlider {
|
||||
public owner: WeakRef<Slider>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<Slider>) {
|
||||
const slider = TNSSlider.new() as TNSSlider;
|
||||
slider.owner = owner;
|
||||
|
||||
return slider;
|
||||
}
|
||||
|
||||
public accessibilityIncrement() {
|
||||
const owner = this.owner.get();
|
||||
if (!owner) {
|
||||
this.value += 10;
|
||||
} else {
|
||||
this.value = owner._handlerAccessibilityIncrementEvent();
|
||||
}
|
||||
|
||||
this.sendActionsForControlEvents(UIControlEvents.ValueChanged);
|
||||
}
|
||||
|
||||
public accessibilityDecrement() {
|
||||
const owner = this.owner.get();
|
||||
if (!owner) {
|
||||
this.value += 10;
|
||||
} else {
|
||||
this.value = owner._handlerAccessibilityDecrementEvent();
|
||||
}
|
||||
|
||||
this.sendActionsForControlEvents(UIControlEvents.ValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class SliderChangeHandlerImpl extends NSObject {
|
||||
private _owner: WeakRef<Slider>;
|
||||
@ -30,11 +65,11 @@ class SliderChangeHandlerImpl extends NSObject {
|
||||
}
|
||||
|
||||
export class Slider extends SliderBase {
|
||||
nativeViewProtected: UISlider;
|
||||
nativeViewProtected: TNSSlider;
|
||||
private _changeHandler: NSObject;
|
||||
|
||||
public createNativeView() {
|
||||
return UISlider.new();
|
||||
public createNativeView(): TNSSlider {
|
||||
return TNSSlider.initWithOwner(new WeakRef(this));
|
||||
}
|
||||
|
||||
public initNativeView(): void {
|
||||
@ -47,7 +82,7 @@ export class Slider extends SliderBase {
|
||||
nativeView.addTargetActionForControlEvents(this._changeHandler, 'sliderValueChanged', UIControlEvents.ValueChanged);
|
||||
}
|
||||
|
||||
public disposeNativeView() {
|
||||
public disposeNativeView(): void {
|
||||
this._changeHandler = null;
|
||||
super.disposeNativeView();
|
||||
}
|
||||
@ -98,4 +133,36 @@ export class Slider extends SliderBase {
|
||||
[backgroundInternalProperty.setNative](value: Background) {
|
||||
//
|
||||
}
|
||||
|
||||
private getAccessibilityStep(): number {
|
||||
if (!this.accessibilityStep || this.accessibilityStep <= 0) {
|
||||
return 10;
|
||||
}
|
||||
|
||||
return this.accessibilityStep;
|
||||
}
|
||||
|
||||
public _handlerAccessibilityIncrementEvent(): number {
|
||||
const args: AccessibilityIncrementEventData = {
|
||||
object: this,
|
||||
eventName: SliderBase.accessibilityIncrementEvent,
|
||||
value: this.value + this.getAccessibilityStep(),
|
||||
};
|
||||
|
||||
this.notify(args);
|
||||
|
||||
return args.value;
|
||||
}
|
||||
|
||||
public _handlerAccessibilityDecrementEvent(): number {
|
||||
const args: AccessibilityDecrementEventData = {
|
||||
object: this,
|
||||
eventName: SliderBase.accessibilityIncrementEvent,
|
||||
value: this.value - this.getAccessibilityStep(),
|
||||
};
|
||||
|
||||
this.notify(args);
|
||||
|
||||
return args.value;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,28 @@
|
||||
import { Slider as SliderDefinition } from '.';
|
||||
import { View, CSSType } from '../core/view';
|
||||
import { Property, CoercibleProperty } from '../core/properties';
|
||||
import { AccessibilityRole } from '../../accessibility';
|
||||
import { CoercibleProperty, Property } from '../core/properties';
|
||||
import { CSSType, View } from '../core/view';
|
||||
|
||||
// TODO: Extract base Range class for slider and progress
|
||||
@CSSType('Slider')
|
||||
export class SliderBase extends View implements SliderDefinition {
|
||||
static readonly accessibilityIncrementEvent = 'accessibilityIncrement';
|
||||
static readonly accessibilityDecrementEvent = 'accessibilityDecrement';
|
||||
|
||||
public value: number;
|
||||
public minValue: number;
|
||||
public maxValue: number;
|
||||
|
||||
get accessibilityStep(): number {
|
||||
return this.style.accessibilityStep;
|
||||
}
|
||||
|
||||
set accessibilityStep(value: number) {
|
||||
this.style.accessibilityStep = value;
|
||||
}
|
||||
|
||||
accessible = true;
|
||||
accessibilityRole = AccessibilityRole.Adjustable;
|
||||
}
|
||||
|
||||
SliderBase.prototype.recycleNativeView = 'auto';
|
||||
|
@ -15,7 +15,7 @@ export abstract class Font implements FontDefinition {
|
||||
return this.fontWeight === FontWeight.SEMI_BOLD || this.fontWeight === FontWeight.BOLD || this.fontWeight === '700' || this.fontWeight === FontWeight.EXTRA_BOLD || this.fontWeight === FontWeight.BLACK;
|
||||
}
|
||||
|
||||
protected constructor(public readonly fontFamily: string, public readonly fontSize: number, public readonly fontStyle: FontStyle, public readonly fontWeight: FontWeight) {}
|
||||
protected constructor(public readonly fontFamily: string, public readonly fontSize: number, public readonly fontStyle: FontStyle, public readonly fontWeight: FontWeight, public readonly fontScale: number) {}
|
||||
|
||||
public abstract getAndroidTypeface(): any /* android.graphics.Typeface */;
|
||||
public abstract getUIFont(defaultFont: any /* UIFont */): any /* UIFont */;
|
||||
@ -23,6 +23,7 @@ export abstract class Font implements FontDefinition {
|
||||
public abstract withFontStyle(style: string): Font;
|
||||
public abstract withFontWeight(weight: string): Font;
|
||||
public abstract withFontSize(size: number): Font;
|
||||
public abstract withFontScale(scale: number): Font;
|
||||
|
||||
public static equals(value1: Font, value2: Font): boolean {
|
||||
// both values are falsy
|
||||
|
@ -15,7 +15,7 @@ export class Font extends FontBase {
|
||||
private _typeface: android.graphics.Typeface;
|
||||
|
||||
constructor(family: string, size: number, style: 'normal' | 'italic', weight: FontWeight) {
|
||||
super(family, size, style, weight);
|
||||
super(family, size, style, weight, 1);
|
||||
}
|
||||
|
||||
public withFontFamily(family: string): Font {
|
||||
@ -34,6 +34,10 @@ export class Font extends FontBase {
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public withFontScale(scale: number): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public getAndroidTypeface(): android.graphics.Typeface {
|
||||
if (!this._typeface) {
|
||||
this._typeface = createTypeface(this);
|
||||
|
2
packages/core/ui/styling/font.d.ts
vendored
2
packages/core/ui/styling/font.d.ts
vendored
@ -5,6 +5,7 @@
|
||||
public fontStyle: FontStyle;
|
||||
public fontWeight: FontWeight;
|
||||
public fontSize: number;
|
||||
public fontScale: number;
|
||||
|
||||
public isBold: boolean;
|
||||
public isItalic: boolean;
|
||||
@ -18,6 +19,7 @@
|
||||
public withFontStyle(style: string): Font;
|
||||
public withFontWeight(weight: string): Font;
|
||||
public withFontSize(size: number): Font;
|
||||
public withFontScale(scale: number): Font;
|
||||
|
||||
public static equals(value1: Font, value2: Font): boolean;
|
||||
}
|
||||
|
@ -11,28 +11,32 @@ const DEFAULT_MONOSPACE = 'Courier New';
|
||||
const SUPPORT_FONT_WEIGHTS = parseFloat(Device.osVersion) >= 10.0;
|
||||
|
||||
export class Font extends FontBase {
|
||||
public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL);
|
||||
public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL, 1);
|
||||
|
||||
private _uiFont: UIFont;
|
||||
|
||||
constructor(family: string, size: number, style: FontStyle, weight: FontWeight) {
|
||||
super(family, size, style, weight);
|
||||
constructor(family: string, size: number, style: FontStyle, weight: FontWeight, scale: number) {
|
||||
super(family, size, style, weight, scale);
|
||||
}
|
||||
|
||||
public withFontFamily(family: string): Font {
|
||||
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight);
|
||||
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontStyle(style: FontStyle): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight);
|
||||
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontWeight(weight: FontWeight): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight);
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontSize(size: number): Font {
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontScale(scale: number): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, this.fontWeight, scale);
|
||||
}
|
||||
|
||||
public getUIFont(defaultFont: UIFont): UIFont {
|
||||
|
@ -1422,6 +1422,27 @@ export const fontFamilyProperty = new InheritedCssProperty<Style, string>({
|
||||
});
|
||||
fontFamilyProperty.register(Style);
|
||||
|
||||
export const fontScaleProperty = new InheritedCssProperty<Style, number>({
|
||||
name: '_fontScale',
|
||||
cssName: '_fontScale',
|
||||
affectsLayout: global.isIOS,
|
||||
valueChanged: (target, oldValue, newValue) => {
|
||||
if (global.isIOS) {
|
||||
if (target.viewRef['handleFontSize'] === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFont = target.fontInternal || Font.default;
|
||||
if (currentFont.fontScale !== newValue) {
|
||||
const newFont = currentFont.withFontScale(newValue);
|
||||
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
||||
}
|
||||
}
|
||||
},
|
||||
valueConverter: (v) => parseFloat(v),
|
||||
});
|
||||
fontScaleProperty.register(Style);
|
||||
|
||||
export const fontSizeProperty = new InheritedCssProperty<Style, number>({
|
||||
name: 'fontSize',
|
||||
cssName: 'font-size',
|
||||
|
@ -11,6 +11,7 @@ import { Observable } from '../../../data/observable';
|
||||
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout';
|
||||
import { Trace } from '../../../trace';
|
||||
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace, TextShadow } from '../../text-base';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
|
||||
import { BoxShadow } from '../box-shadow';
|
||||
|
||||
export interface CommonLayoutParams {
|
||||
@ -98,6 +99,7 @@ export class Style extends Observable implements StyleDefinition {
|
||||
}
|
||||
|
||||
public fontInternal: Font;
|
||||
public _fontScale: number;
|
||||
public backgroundInternal: Background;
|
||||
|
||||
public rotate: number;
|
||||
@ -211,6 +213,16 @@ export class Style extends Observable implements StyleDefinition {
|
||||
public flexWrapBefore: FlexWrapBefore;
|
||||
public alignSelf: AlignSelf;
|
||||
|
||||
// Accessibility properties
|
||||
public accessible: boolean;
|
||||
public accessibilityHidden: boolean;
|
||||
public accessibilityRole: AccessibilityRole;
|
||||
public accessibilityState: AccessibilityState;
|
||||
public accessibilityLiveRegion: AccessibilityLiveRegion;
|
||||
public accessibilityLanguage: string;
|
||||
public accessibilityMediaSession: boolean;
|
||||
public accessibilityStep: number;
|
||||
|
||||
public PropertyBag: {
|
||||
new (): { [property: string]: string };
|
||||
prototype: { [property: string]: string };
|
||||
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item type="id" name="nativescript_accessibility_id"/>
|
||||
</resources>
|
Reference in New Issue
Block a user