Files
NativeScript/packages/core/accessibility/accessibility-common.ts
2025-07-08 22:18:43 -07:00

516 lines
12 KiB
TypeScript

import type { EventData, EventDataValue } from '../data/observable';
import { Observable } from '../data/observable';
import type { View } from '../ui/core/view';
import type { Page } from '../ui/page';
const lastFocusedViewOnPageKeyName = '__lastFocusedViewOnPage';
export const accessibilityBlurEvent = 'accessibilityBlur';
export const accessibilityFocusEvent = 'accessibilityFocus';
export const accessibilityFocusChangedEvent = 'accessibilityFocusChanged';
export const accessibilityPerformEscapeEvent = 'accessibilityPerformEscape';
/**
* 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 EventDataValue);
if (receivedFocus) {
if (view.page) {
view.page[lastFocusedViewOnPageKeyName] = new WeakRef(view);
}
view.notify({
eventName: accessibilityFocusEvent,
object: view,
} as EventData);
} else if (lostFocus) {
view.notify({
eventName: accessibilityBlurEvent,
object: view,
} as EventData);
}
}
export function getLastFocusedViewOnPage(page: Page): View | null {
try {
const lastFocusedViewRef = page[lastFocusedViewOnPageKeyName] as WeakRef<View>;
if (!lastFocusedViewRef) {
return null;
}
const lastFocusedView = lastFocusedViewRef.deref();
if (!lastFocusedView) {
return null;
}
if (!lastFocusedView.parent || lastFocusedView.page !== page) {
return null;
}
return lastFocusedView;
} catch {
// ignore
} finally {
delete page[lastFocusedViewOnPageKeyName];
}
return null;
}
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);
}
}
let a11yServiceEnabled: boolean;
export function isA11yEnabled(): boolean {
if (typeof a11yServiceEnabled === 'boolean') {
return a11yServiceEnabled;
}
return undefined;
}
export function setA11yEnabled(value: boolean): void {
a11yServiceEnabled = value;
}
export 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 [];
}
export const VALID_FONT_SCALES = __APPLE__ // Apple 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))[0];
}
export enum FontScaleCategory {
ExtraSmall = 'extra-small',
Medium = 'medium',
ExtraLarge = 'extra-large',
}
export const fontScaleExtraSmallCategoryClass = `a11y-fontscale-xs`;
export const fontScaleMediumCategoryClass = `a11y-fontscale-m`;
export const fontScaleExtraLargeCategoryClass = `a11y-fontscale-xl`;
export const fontScaleCategoryClasses = [fontScaleExtraSmallCategoryClass, fontScaleMediumCategoryClass, fontScaleExtraLargeCategoryClass];
export const a11yServiceEnabledClass = `a11y-service-enabled`;
export const a11yServiceDisabledClass = `a11y-service-disabled`;
export const a11yServiceClasses = [a11yServiceEnabledClass, a11yServiceDisabledClass];
let currentFontScale: number = null;
export function setFontScale(scale: number) {
currentFontScale = scale;
}
export function getFontScale() {
return currentFontScale;
}
export function getFontScaleCategory(): FontScaleCategory {
if (__ANDROID__) {
return FontScaleCategory.Medium;
}
if (getFontScale() < 0.85) {
return FontScaleCategory.ExtraSmall;
}
if (getFontScale() > 1.5) {
return FontScaleCategory.ExtraLarge;
}
return FontScaleCategory.Medium;
}
let initAccessibilityCssHelperCallback: () => void;
export function setInitAccessibilityCssHelper(callback: () => void) {
initAccessibilityCssHelperCallback = callback;
}
export function readyInitAccessibilityCssHelper() {
if (initAccessibilityCssHelperCallback) {
initAccessibilityCssHelperCallback();
initAccessibilityCssHelperCallback = null;
}
}
let initFontScaleCallback: () => void;
export function setInitFontScale(callback: () => void) {
initFontScaleCallback = callback;
}
export function readyInitFontScale() {
if (initFontScaleCallback) {
initFontScaleCallback();
initFontScaleCallback = null;
}
}
let fontScaleCssClasses: Map<number, string>;
export function setFontScaleCssClasses(value: Map<number, string>) {
fontScaleCssClasses = value;
}
export function getFontScaleCssClasses() {
return fontScaleCssClasses;
}
let currentFontScaleClass = '';
export function setCurrentFontScaleClass(value: string) {
currentFontScaleClass = value;
}
export function getCurrentFontScaleClass() {
return currentFontScaleClass;
}
let currentFontScaleCategory = '';
export function setCurrentFontScaleCategory(value: string) {
currentFontScaleCategory = value;
}
export function getCurrentFontScaleCategory() {
return currentFontScaleCategory;
}
let currentA11YServiceClass = '';
export function setCurrentA11YServiceClass(value: string) {
currentA11YServiceClass = value;
}
export function getCurrentA11YServiceClass() {
return currentA11YServiceClass;
}
export enum AccessibilityTrait {
/**
* 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 not enabled and does not respond to user interaction.
*/
NotEnabled = 'disabled',
/**
* The element is currently selected.
*/
Selected = 'selected',
/**
* The element frequently updates its label or value.
*/
UpdatesFrequently = 'frequentUpdates',
}
export enum AccessibilityRole {
/**
* The element allows continuous adjustment through a range of values.
*/
Adjustable = 'adjustable',
/**
* The element should be treated as a button.
*/
Button = 'button',
/**
* The element behaves like a Checkbox
*/
Checkbox = 'checkbox',
/**
* The element is a header that divides content into sections, such as the title of a navigation bar.
*/
Header = 'header',
/**
* 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 a link.
*/
Link = 'link',
/**
* The element has no traits.
*/
None = 'none',
/**
* The element plays its own sound when activated.
*/
PlaysSound = 'plays',
/**
* The element behaves like a ProgressBar
*/
ProgressBar = 'progressBar',
/**
* The element behaves like a RadioButton
*/
RadioButton = 'radioButton',
/**
* The element should be treated as a search field.
*/
Search = 'search',
/**
* The element behaves like a SpinButton
*/
SpinButton = 'spinButton',
/**
* The element starts a media session when it is activated.
*/
StartsMediaSession = 'startsMedia',
/**
* The element should be treated as static text that cannot change.
*/
StaticText = 'text',
/**
* The element provides summary information when the application starts.
*/
Summary = 'summary',
/**
* 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 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',
}
export interface AccessibilityEventPerformEscape extends EventData {
cancel?: boolean;
}
export interface AccessibilityEventOptions {
androidAccessibilityEvent?: AndroidAccessibilityEvent;
iosNotificationType?: IOSPostAccessibilityNotificationType;
message?: string;
}