mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
refactor: circular deps part 7
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { EventData, EventDataValue } from '../data/observable';
|
||||
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';
|
||||
|
||||
@@ -72,3 +73,443 @@ export function getLastFocusedViewOnPage(page: Page): View | null {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// Only keep helpers that do not import from application here.
|
||||
// If any code remains that needs Application, move it to accessibility-css-init.ts.
|
||||
|
||||
export { initAccessibilityCssHelper } from './accessibility-css-init';
|
||||
@@ -1,121 +0,0 @@
|
||||
import { Application } from '../application';
|
||||
import type { View } from '../ui/core/view';
|
||||
import { AccessibilityServiceEnabledObservable } from './accessibility-service';
|
||||
import { FontScaleCategory, getCurrentFontScale, getFontScaleCategory, VALID_FONT_SCALES } from './font-scale';
|
||||
|
||||
// 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 = Application.getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
Application.applyCssClass(rootView, cssClasses, newCssClass);
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => Application.applyCssClass(rootModalView, cssClasses, newCssClass));
|
||||
}
|
||||
|
||||
function applyFontScaleToRootViews(): void {
|
||||
const rootView = Application.getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontScale = getCurrentFontScale();
|
||||
|
||||
rootView.style.fontScaleInternal = fontScale;
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => (rootModalView.style.fontScaleInternal = fontScale));
|
||||
}
|
||||
|
||||
function updateCurrentHelperClasses(): void {
|
||||
const fontScale = getCurrentFontScale();
|
||||
const fontScaleCategory = getFontScaleCategory();
|
||||
|
||||
const oldFontScaleClass = currentFontScaleClass;
|
||||
if (fontScaleCssClasses.has(fontScale)) {
|
||||
currentFontScaleClass = fontScaleCssClasses.get(fontScale);
|
||||
} else {
|
||||
currentFontScaleClass = fontScaleCssClasses.get(1);
|
||||
}
|
||||
|
||||
if (oldFontScaleClass !== currentFontScaleClass) {
|
||||
applyRootCssClass([...fontScaleCssClasses.values()], currentFontScaleClass);
|
||||
}
|
||||
|
||||
const oldActiveFontScaleCategory = currentFontScaleCategory;
|
||||
switch (fontScaleCategory) {
|
||||
case FontScaleCategory.ExtraSmall: {
|
||||
currentFontScaleCategory = fontScaleExtraSmallCategoryClass;
|
||||
break;
|
||||
}
|
||||
case FontScaleCategory.Medium: {
|
||||
currentFontScaleCategory = fontScaleMediumCategoryClass;
|
||||
break;
|
||||
}
|
||||
case FontScaleCategory.ExtraLarge: {
|
||||
currentFontScaleCategory = fontScaleExtraLargeCategoryClass;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
currentFontScaleCategory = fontScaleMediumCategoryClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldActiveFontScaleCategory !== currentFontScaleCategory) {
|
||||
applyRootCssClass(fontScaleCategoryClasses, currentFontScaleCategory);
|
||||
}
|
||||
|
||||
const oldA11YStatusClass = currentA11YServiceClass;
|
||||
if (accessibilityServiceObservable.accessibilityServiceEnabled) {
|
||||
currentA11YServiceClass = a11yServiceEnabledClass;
|
||||
} else {
|
||||
currentA11YServiceClass = a11yServiceDisabledClass;
|
||||
}
|
||||
|
||||
if (oldA11YStatusClass !== currentA11YServiceClass) {
|
||||
applyRootCssClass(a11yServiceClasses, currentA11YServiceClass);
|
||||
}
|
||||
}
|
||||
|
||||
export function initAccessibilityCssHelper(): void {
|
||||
ensureClasses();
|
||||
|
||||
Application.on(Application.fontScaleChangedEvent, () => {
|
||||
updateCurrentHelperClasses();
|
||||
|
||||
applyFontScaleToRootViews();
|
||||
});
|
||||
|
||||
accessibilityServiceObservable.on(AccessibilityServiceEnabledObservable.propertyChangeEvent, updateCurrentHelperClasses);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { CssProperty, InheritedCssProperty, Property } from '../ui/core/properti
|
||||
import type { View } from '../ui/core/view';
|
||||
import { booleanConverter } from '../ui/core/view-base/utils';
|
||||
import { Style } from '../ui/styling/style';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-types';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from './accessibility-common';
|
||||
|
||||
function makePropertyEnumConverter<T>(enumValues) {
|
||||
return (value: string): T | null => {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { Application, ApplicationEventData } from '../application';
|
||||
import { Observable } from '../data/observable';
|
||||
import { Trace } from '../trace';
|
||||
import * as Utils from '../utils';
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
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;
|
||||
|
||||
// @ts-ignore todo: fix
|
||||
get accessibilityServiceEnabled(): boolean {
|
||||
return !!this[accessibilityStateEnabledPropName] && !!this[touchExplorationStateEnabledPropName];
|
||||
}
|
||||
|
||||
set accessibilityServiceEnabled(v) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let accessibilityStateChangeListener: android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
|
||||
let touchExplorationStateChangeListener: android.view.accessibility.AccessibilityManager.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);
|
||||
}
|
||||
},
|
||||
});
|
||||
accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
|
||||
|
||||
if (SDK_VERSION >= 19) {
|
||||
touchExplorationStateChangeListener = new android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener({
|
||||
onTouchExplorationStateChanged(enabled) {
|
||||
updateAccessibilityState();
|
||||
|
||||
if (Trace.isEnabled()) {
|
||||
Trace.write(`TouchExplorationStateChangeListener state changed to: ${!!enabled}`, Trace.categories.Accessibility);
|
||||
}
|
||||
},
|
||||
});
|
||||
accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
|
||||
}
|
||||
|
||||
updateAccessibilityState();
|
||||
|
||||
Application.on(Application.resumeEvent, updateAccessibilityState);
|
||||
Application.on(Application.exitEvent, (args: 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) {
|
||||
accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
accessibilityStateChangeListener = null;
|
||||
touchExplorationStateChangeListener = null;
|
||||
|
||||
if (sharedA11YObservable) {
|
||||
sharedA11YObservable.removeEventListener(Observable.propertyChangeEvent);
|
||||
sharedA11YObservable = null;
|
||||
}
|
||||
|
||||
Application.off(Application.resumeEvent, updateAccessibilityState);
|
||||
});
|
||||
|
||||
return sharedA11YObservable;
|
||||
}
|
||||
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
return ensureStateListener().accessibilityServiceEnabled;
|
||||
}
|
||||
|
||||
export class AccessibilityServiceEnabledObservable extends CommonA11YServiceEnabledObservable {
|
||||
constructor() {
|
||||
super(ensureStateListener());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
@@ -1,75 +0,0 @@
|
||||
import { 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());
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import type { EventData } from '../data/observable';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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',
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Application, ApplicationEventData } 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,
|
||||
newValue: currentFontScale,
|
||||
} as ApplicationEventData);
|
||||
}
|
||||
}
|
||||
|
||||
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
9
packages/core/accessibility/font-scale.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
@@ -1,110 +0,0 @@
|
||||
import { 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,
|
||||
newValue: currentFontScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,690 +0,0 @@
|
||||
import { Application, ApplicationEventData } from '../application';
|
||||
import { Trace } from '../trace';
|
||||
import { SDK_VERSION } from '../utils/constants';
|
||||
import { android as androidUtils } from '../utils';
|
||||
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 (SDK_VERSION >= 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;
|
||||
}
|
||||
|
||||
// Set resource id that can be used with test frameworks without polluting the content description.
|
||||
const id = host.getTag(androidUtils.resources.getId(`:id/nativescript_accessibility_id`));
|
||||
if (id != null) {
|
||||
info.setViewIdResourceName(id);
|
||||
}
|
||||
|
||||
const accessibilityRole = view.accessibilityRole;
|
||||
if (accessibilityRole) {
|
||||
const androidClassName = RoleTypeMap.get(accessibilityRole);
|
||||
if (androidClassName) {
|
||||
const oldClassName = info.getClassName() || (SDK_VERSION >= 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 (SDK_VERSION >= 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) {
|
||||
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: 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);
|
||||
}
|
||||
|
||||
let updateAccessibilityPropertiesMicroTask;
|
||||
let pendingViews = new Set<View>();
|
||||
export function updateAccessibilityProperties(view: View) {
|
||||
if (!view.nativeViewProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingViews.add(view);
|
||||
if (updateAccessibilityPropertiesMicroTask) return;
|
||||
|
||||
updateAccessibilityPropertiesMicroTask = true;
|
||||
Promise.resolve().then(() => {
|
||||
updateAccessibilityPropertiesMicroTask = false;
|
||||
let _pendingViews = Array.from(pendingViews);
|
||||
pendingViews = new Set();
|
||||
for (const view of _pendingViews) {
|
||||
if (!view.nativeViewProtected) continue;
|
||||
setAccessibilityDelegate(view);
|
||||
applyContentDescription(view);
|
||||
}
|
||||
_pendingViews = [];
|
||||
});
|
||||
}
|
||||
|
||||
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 || !androidView.setAccessibilityDelegate) {
|
||||
return;
|
||||
}
|
||||
|
||||
androidViewToTNSView.set(androidView, new WeakRef(view));
|
||||
|
||||
let hasOldDelegate = false;
|
||||
if (typeof androidView.getAccessibilityDelegate === 'function') {
|
||||
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 || (androidView instanceof android.widget.TextView && !view._androidContentDescriptionUpdated)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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})`;
|
||||
|
||||
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;
|
||||
}
|
||||
37
packages/core/accessibility/index.d.ts
vendored
37
packages/core/accessibility/index.d.ts
vendored
@@ -1,37 +0,0 @@
|
||||
import type { Page } from '../ui/page';
|
||||
import type { View } from '../ui/core/view';
|
||||
import type { 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;
|
||||
|
||||
/**
|
||||
* Android: Update the content description for 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;
|
||||
@@ -1,276 +0,0 @@
|
||||
import { Application } from '../application';
|
||||
import type { ViewBase } from '../ui/core/view-base';
|
||||
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.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction],
|
||||
[AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn],
|
||||
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
|
||||
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
|
||||
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
|
||||
]);
|
||||
|
||||
RoleTypeMap = new Map<AccessibilityRole, number>([
|
||||
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
|
||||
[AccessibilityRole.Button, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Header, UIAccessibilityTraitHeader],
|
||||
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
|
||||
[AccessibilityRole.Image, UIAccessibilityTraitImage],
|
||||
[AccessibilityRole.ImageButton, UIAccessibilityTraitImage | UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Link, UIAccessibilityTraitLink],
|
||||
[AccessibilityRole.None, UIAccessibilityTraitNone],
|
||||
[AccessibilityRole.PlaysSound, UIAccessibilityTraitPlaysSound],
|
||||
[AccessibilityRole.RadioButton, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Search, UIAccessibilityTraitSearchField],
|
||||
[AccessibilityRole.StaticText, UIAccessibilityTraitStaticText],
|
||||
[AccessibilityRole.StartsMediaSession, UIAccessibilityTraitStartsMediaSession],
|
||||
[AccessibilityRole.Summary, UIAccessibilityTraitSummaryElement],
|
||||
[AccessibilityRole.Switch, 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?.deref();
|
||||
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;
|
||||
}
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// console.log('--- Accessible element: ', view.constructor.name);
|
||||
// console.log('accessibilityLabel: ', view.accessibilityLabel);
|
||||
// console.log('accessibilityRole: ', accessibilityRole);
|
||||
// console.log('accessibilityState: ', accessibilityState);
|
||||
// console.log('accessibilityValue: ', view.accessibilityValue);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// if (view.accessibilityLiveRegion) {
|
||||
// console.log('accessibilityLiveRegion:', view.accessibilityLiveRegion);
|
||||
// }
|
||||
|
||||
if (view.accessibilityMediaSession) {
|
||||
a11yTraits |= RoleTypeMap.get(AccessibilityRole.StartsMediaSession);
|
||||
}
|
||||
|
||||
// NOTE: There were duplicated types in traits and roles previously which we conslidated
|
||||
// not sure if this is still needed
|
||||
// accessibilityTraits used to be stored on {N} view component but if the above
|
||||
// is combining all traits fresh each time through, don't believe we need to keep track or previous traits
|
||||
// if (view.accessibilityTraits) {
|
||||
// a11yTraits |= inputArrayToBitMask(view.accessibilityTraits, AccessibilityTraitsMap);
|
||||
// }
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// console.log('a11yTraits:', a11yTraits);
|
||||
// console.log(' ');
|
||||
|
||||
uiView.accessibilityTraits = a11yTraits;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
1
packages/core/accessibility/index.ts
Normal file
1
packages/core/accessibility/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './accessibility-common';
|
||||
@@ -1,5 +1,3 @@
|
||||
import { initAccessibilityCssHelper } from '../accessibility/accessibility-css-init';
|
||||
import { initAccessibilityFontScale } from '../accessibility/font-scale';
|
||||
import { CoreTypes } from '../core-types';
|
||||
import { CSSUtils } from '../css/system-classes';
|
||||
import { Device, Screen } from '../platform';
|
||||
@@ -11,8 +9,9 @@ import type { View } from '../ui/core/view';
|
||||
import type { Frame } from '../ui/frame';
|
||||
import type { NavigationEntry } from '../ui/frame/frame-interfaces';
|
||||
import type { StyleScope } from '../ui/styling/style-scope';
|
||||
import type { AndroidApplication as IAndroidApplication, iOSApplication as IiOSApplication } from './';
|
||||
import type { AndroidApplication as AndroidApplicationType, iOSApplication as iOSApplicationType } from '.';
|
||||
import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData, FontScaleChangedEventData, InitRootViewEventData, LaunchEventData, LoadAppCSSEventData, NativeScriptError, OrientationChangedEventData, SystemAppearanceChangedEventData, UnhandledErrorEventData } from './application-interfaces';
|
||||
import { readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common';
|
||||
|
||||
// prettier-ignore
|
||||
const ORIENTATION_CSS_CLASSES = [
|
||||
@@ -371,8 +370,8 @@ export class ApplicationCommon {
|
||||
|
||||
initRootView(rootView: View) {
|
||||
this.setRootViewCSSClasses(rootView);
|
||||
initAccessibilityCssHelper();
|
||||
initAccessibilityFontScale();
|
||||
readyInitAccessibilityCssHelper();
|
||||
readyInitFontScale();
|
||||
this.notify(<InitRootViewEventData>{ eventName: this.initRootViewEvent, rootView });
|
||||
}
|
||||
|
||||
@@ -614,11 +613,11 @@ export class ApplicationCommon {
|
||||
|
||||
public started = false;
|
||||
|
||||
get android(): IAndroidApplication {
|
||||
get android(): AndroidApplicationType {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get ios(): IiOSApplication {
|
||||
get ios(): iOSApplicationType {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
35
packages/core/application/application.d.ts
vendored
35
packages/core/application/application.d.ts
vendored
@@ -1,4 +1,5 @@
|
||||
import { ApplicationCommon } from './application-common';
|
||||
import { FontScaleCategory } from '../accessibility/font-scale-common';
|
||||
|
||||
export * from './application-common';
|
||||
export * from './application-interfaces';
|
||||
@@ -192,3 +193,37 @@ export class iOSApplication extends ApplicationCommon {
|
||||
*/
|
||||
removeNotificationObserver(observer: any, notificationName: string);
|
||||
}
|
||||
|
||||
export const VALID_FONT_SCALES: number[];
|
||||
export function getCurrentFontScale(): number;
|
||||
export function getAndroidAccessibilityManager(): android.view.accessibility.AccessibilityManager | null;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Android: Update the content description for 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;
|
||||
|
||||
@@ -1,12 +1,49 @@
|
||||
import { profile } from '../profiling';
|
||||
import { View } from '../ui/core/view';
|
||||
import type { View } from '../ui/core/view';
|
||||
import { isEmbedded } from '../ui/embedding';
|
||||
import { IOSHelper } from '../ui/core/view/view-helper';
|
||||
import { NavigationEntry } from '../ui/frame/frame-interfaces';
|
||||
import * as Utils from '../utils';
|
||||
import type { iOSApplication as IiOSApplication } from './application';
|
||||
import { ApplicationCommon } from './application-common';
|
||||
import { ApplicationEventData } from './application-interfaces';
|
||||
import { Observable } from '../data/observable';
|
||||
import { Trace } from '../trace';
|
||||
import {
|
||||
AccessibilityServiceEnabledPropName,
|
||||
CommonA11YServiceEnabledObservable,
|
||||
SharedA11YObservable,
|
||||
a11yServiceClasses,
|
||||
a11yServiceDisabledClass,
|
||||
a11yServiceEnabledClass,
|
||||
fontScaleCategoryClasses,
|
||||
fontScaleExtraLargeCategoryClass,
|
||||
fontScaleExtraSmallCategoryClass,
|
||||
fontScaleMediumCategoryClass,
|
||||
getCurrentA11YServiceClass,
|
||||
getCurrentFontScaleCategory,
|
||||
getCurrentFontScaleClass,
|
||||
getFontScaleCssClasses,
|
||||
setCurrentA11YServiceClass,
|
||||
setCurrentFontScaleCategory,
|
||||
setCurrentFontScaleClass,
|
||||
setFontScaleCssClasses,
|
||||
FontScaleCategory,
|
||||
getClosestValidFontScale,
|
||||
VALID_FONT_SCALES,
|
||||
setFontScale,
|
||||
getFontScale,
|
||||
setInitFontScale,
|
||||
getFontScaleCategory,
|
||||
setInitAccessibilityCssHelper,
|
||||
notifyAccessibilityFocusState,
|
||||
AccessibilityLiveRegion,
|
||||
AccessibilityRole,
|
||||
AccessibilityState,
|
||||
AccessibilityTrait,
|
||||
isA11yEnabled,
|
||||
setA11yEnabled,
|
||||
enforceArray,
|
||||
} from '../accessibility/accessibility-common';
|
||||
|
||||
@NativeClass
|
||||
class CADisplayLinkTarget extends NSObject {
|
||||
@@ -73,7 +110,7 @@ class Responder extends UIResponder implements UIApplicationDelegate {
|
||||
static ObjCProtocols = [UIApplicationDelegate];
|
||||
}
|
||||
|
||||
export class iOSApplication extends ApplicationCommon implements IiOSApplication {
|
||||
export class iOSApplication extends ApplicationCommon {
|
||||
private _delegate: UIApplicationDelegate;
|
||||
private _delegateHandlers = new Map<string, Array<Function>>();
|
||||
private _window: UIWindow;
|
||||
@@ -514,3 +551,516 @@ global.__onLiveSyncCore = function (context?: ModuleContext) {
|
||||
export * from './application-common';
|
||||
export const Application = iosApp;
|
||||
export const AndroidApplication = undefined;
|
||||
|
||||
function fontScaleChanged(origFontScale: number) {
|
||||
const oldValue = getFontScale();
|
||||
setFontScale(getClosestValidFontScale(origFontScale));
|
||||
const currentFontScale = getFontScale();
|
||||
|
||||
if (oldValue !== currentFontScale) {
|
||||
Application.notify({
|
||||
eventName: Application.fontScaleChangedEvent,
|
||||
object: Application,
|
||||
newValue: currentFontScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentFontScale(): number {
|
||||
setupConfigListener();
|
||||
|
||||
return getFontScale();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
setInitFontScale(setupConfigListener);
|
||||
|
||||
/**
|
||||
* 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.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction],
|
||||
[AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn],
|
||||
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
|
||||
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
|
||||
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
|
||||
]);
|
||||
|
||||
RoleTypeMap = new Map<AccessibilityRole, number>([
|
||||
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
|
||||
[AccessibilityRole.Button, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Header, UIAccessibilityTraitHeader],
|
||||
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
|
||||
[AccessibilityRole.Image, UIAccessibilityTraitImage],
|
||||
[AccessibilityRole.ImageButton, UIAccessibilityTraitImage | UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Link, UIAccessibilityTraitLink],
|
||||
[AccessibilityRole.None, UIAccessibilityTraitNone],
|
||||
[AccessibilityRole.PlaysSound, UIAccessibilityTraitPlaysSound],
|
||||
[AccessibilityRole.RadioButton, UIAccessibilityTraitButton],
|
||||
[AccessibilityRole.Search, UIAccessibilityTraitSearchField],
|
||||
[AccessibilityRole.StaticText, UIAccessibilityTraitStaticText],
|
||||
[AccessibilityRole.StartsMediaSession, UIAccessibilityTraitStartsMediaSession],
|
||||
[AccessibilityRole.Summary, UIAccessibilityTraitSummaryElement],
|
||||
[AccessibilityRole.Switch, 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?.deref();
|
||||
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;
|
||||
}
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// console.log('--- Accessible element: ', view.constructor.name);
|
||||
// console.log('accessibilityLabel: ', view.accessibilityLabel);
|
||||
// console.log('accessibilityRole: ', accessibilityRole);
|
||||
// console.log('accessibilityState: ', accessibilityState);
|
||||
// console.log('accessibilityValue: ', view.accessibilityValue);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// if (view.accessibilityLiveRegion) {
|
||||
// console.log('accessibilityLiveRegion:', view.accessibilityLiveRegion);
|
||||
// }
|
||||
|
||||
if (view.accessibilityMediaSession) {
|
||||
a11yTraits |= RoleTypeMap.get(AccessibilityRole.StartsMediaSession);
|
||||
}
|
||||
|
||||
// NOTE: There were duplicated types in traits and roles previously which we conslidated
|
||||
// not sure if this is still needed
|
||||
// accessibilityTraits used to be stored on {N} view component but if the above
|
||||
// is combining all traits fresh each time through, don't believe we need to keep track or previous traits
|
||||
// if (view.accessibilityTraits) {
|
||||
// a11yTraits |= inputArrayToBitMask(view.accessibilityTraits, AccessibilityTraitsMap);
|
||||
// }
|
||||
|
||||
// NOTE: left here for various core inspection passes while running the toolbox app
|
||||
// console.log('a11yTraits:', a11yTraits);
|
||||
// console.log(' ');
|
||||
|
||||
uiView.accessibilityTraits = a11yTraits;
|
||||
}
|
||||
|
||||
export const sendAccessibilityEvent = (): void => {};
|
||||
export const updateContentDescription = (): string | null => null;
|
||||
|
||||
export function isAccessibilityServiceEnabled(): boolean {
|
||||
const accessibilityServiceEnabled = isA11yEnabled();
|
||||
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') {
|
||||
setA11yEnabled(false);
|
||||
return isA11yEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
setA11yEnabled(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, () => {
|
||||
setA11yEnabled(isVoiceOverRunning());
|
||||
});
|
||||
|
||||
Application.on(Application.exitEvent, () => {
|
||||
if (nativeObserver) {
|
||||
Application.ios.removeNotificationObserver(nativeObserver, voiceOverStatusChangedNotificationName);
|
||||
}
|
||||
|
||||
setA11yEnabled(undefined);
|
||||
nativeObserver = null;
|
||||
});
|
||||
}
|
||||
|
||||
Application.on(Application.resumeEvent, () => {
|
||||
setA11yEnabled(isVoiceOverRunning());
|
||||
});
|
||||
|
||||
return isA11yEnabled();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
let accessibilityServiceObservable: AccessibilityServiceEnabledObservable;
|
||||
export function ensureClasses() {
|
||||
if (accessibilityServiceObservable) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFontScaleCssClasses(new Map(VALID_FONT_SCALES.map((fs) => [fs, `a11y-fontscale-${Number(fs * 100).toFixed(0)}`])));
|
||||
|
||||
accessibilityServiceObservable = new AccessibilityServiceEnabledObservable();
|
||||
}
|
||||
|
||||
export function updateCurrentHelperClasses(applyRootCssClass: (cssClasses: string[], newCssClass: string) => void): void {
|
||||
const fontScale = getFontScale();
|
||||
const fontScaleCategory = getFontScaleCategory();
|
||||
const fontScaleCssClasses = getFontScaleCssClasses();
|
||||
const oldFontScaleClass = getCurrentFontScaleClass();
|
||||
if (fontScaleCssClasses.has(fontScale)) {
|
||||
setCurrentFontScaleClass(fontScaleCssClasses.get(fontScale));
|
||||
} else {
|
||||
setCurrentFontScaleClass(fontScaleCssClasses.get(1));
|
||||
}
|
||||
|
||||
if (oldFontScaleClass !== getCurrentFontScaleClass()) {
|
||||
applyRootCssClass([...fontScaleCssClasses.values()], getCurrentFontScaleClass());
|
||||
}
|
||||
|
||||
const oldActiveFontScaleCategory = getCurrentFontScaleCategory();
|
||||
switch (fontScaleCategory) {
|
||||
case FontScaleCategory.ExtraSmall: {
|
||||
setCurrentFontScaleCategory(fontScaleExtraSmallCategoryClass);
|
||||
break;
|
||||
}
|
||||
case FontScaleCategory.Medium: {
|
||||
setCurrentFontScaleCategory(fontScaleMediumCategoryClass);
|
||||
break;
|
||||
}
|
||||
case FontScaleCategory.ExtraLarge: {
|
||||
setCurrentFontScaleCategory(fontScaleExtraLargeCategoryClass);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setCurrentFontScaleCategory(fontScaleMediumCategoryClass);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldActiveFontScaleCategory !== getCurrentFontScaleCategory()) {
|
||||
applyRootCssClass(fontScaleCategoryClasses, getCurrentFontScaleCategory());
|
||||
}
|
||||
|
||||
const oldA11YStatusClass = getCurrentA11YServiceClass();
|
||||
if (accessibilityServiceObservable.accessibilityServiceEnabled) {
|
||||
setCurrentA11YServiceClass(a11yServiceEnabledClass);
|
||||
} else {
|
||||
setCurrentA11YServiceClass(a11yServiceDisabledClass);
|
||||
}
|
||||
|
||||
if (oldA11YStatusClass !== getCurrentA11YServiceClass()) {
|
||||
applyRootCssClass(a11yServiceClasses, getCurrentA11YServiceClass());
|
||||
}
|
||||
}
|
||||
|
||||
function applyRootCssClass(cssClasses: string[], newCssClass: string): void {
|
||||
const rootView = Application.getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
Application.applyCssClass(rootView, cssClasses, newCssClass);
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => Application.applyCssClass(rootModalView, cssClasses, newCssClass));
|
||||
}
|
||||
|
||||
function applyFontScaleToRootViews(): void {
|
||||
const rootView = Application.getRootView();
|
||||
if (!rootView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fontScale = getCurrentFontScale();
|
||||
|
||||
rootView.style.fontScaleInternal = fontScale;
|
||||
|
||||
const rootModalViews = <Array<View>>rootView._getRootModalViews();
|
||||
rootModalViews.forEach((rootModalView) => (rootModalView.style.fontScaleInternal = fontScale));
|
||||
}
|
||||
|
||||
export function initAccessibilityCssHelper(): void {
|
||||
ensureClasses();
|
||||
|
||||
Application.on(Application.fontScaleChangedEvent, () => {
|
||||
updateCurrentHelperClasses(applyRootCssClass);
|
||||
applyFontScaleToRootViews();
|
||||
});
|
||||
|
||||
accessibilityServiceObservable.on(AccessibilityServiceEnabledObservable.propertyChangeEvent, () => updateCurrentHelperClasses(applyRootCssClass));
|
||||
}
|
||||
setInitAccessibilityCssHelper(initAccessibilityCssHelper);
|
||||
|
||||
@@ -1,641 +0,0 @@
|
||||
var g =
|
||||
(typeof globalThis !== 'undefined' && globalThis) ||
|
||||
(typeof self !== 'undefined' && self) ||
|
||||
// eslint-disable-next-line no-undef
|
||||
(typeof global !== 'undefined' && global) ||
|
||||
{}
|
||||
|
||||
var support = {
|
||||
searchParams: 'URLSearchParams' in g,
|
||||
iterable: 'Symbol' in g && 'iterator' in Symbol,
|
||||
blob:
|
||||
'FileReader' in g &&
|
||||
'Blob' in g &&
|
||||
(function() {
|
||||
try {
|
||||
new Blob()
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
})(),
|
||||
formData: 'FormData' in g,
|
||||
arrayBuffer: 'ArrayBuffer' in g
|
||||
}
|
||||
|
||||
function isDataView(obj) {
|
||||
return obj && DataView.prototype.isPrototypeOf(obj)
|
||||
}
|
||||
|
||||
if (support.arrayBuffer) {
|
||||
var viewClasses = [
|
||||
'[object Int8Array]',
|
||||
'[object Uint8Array]',
|
||||
'[object Uint8ClampedArray]',
|
||||
'[object Int16Array]',
|
||||
'[object Uint16Array]',
|
||||
'[object Int32Array]',
|
||||
'[object Uint32Array]',
|
||||
'[object Float32Array]',
|
||||
'[object Float64Array]'
|
||||
]
|
||||
|
||||
var isArrayBufferView =
|
||||
ArrayBuffer.isView ||
|
||||
function(obj) {
|
||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeName(name) {
|
||||
if (typeof name !== 'string') {
|
||||
name = String(name)
|
||||
}
|
||||
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
|
||||
throw new TypeError('Invalid character in header field name: "' + name + '"')
|
||||
}
|
||||
return name.toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
if (typeof value !== 'string') {
|
||||
value = String(value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Build a destructive iterator for the value list
|
||||
function iteratorFor(items) {
|
||||
var iterator = {
|
||||
next: function() {
|
||||
var value = items.shift()
|
||||
return {done: value === undefined, value: value}
|
||||
}
|
||||
}
|
||||
|
||||
if (support.iterable) {
|
||||
iterator[Symbol.iterator] = function() {
|
||||
return iterator
|
||||
}
|
||||
}
|
||||
|
||||
return iterator
|
||||
}
|
||||
|
||||
export function Headers(headers) {
|
||||
this.map = {}
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach(function(value, name) {
|
||||
this.append(name, value)
|
||||
}, this)
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.forEach(function(header) {
|
||||
if (header.length != 2) {
|
||||
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)
|
||||
}
|
||||
this.append(header[0], header[1])
|
||||
}, this)
|
||||
} else if (headers) {
|
||||
Object.getOwnPropertyNames(headers).forEach(function(name) {
|
||||
this.append(name, headers[name])
|
||||
}, this)
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype.append = function(name, value) {
|
||||
name = normalizeName(name)
|
||||
value = normalizeValue(value)
|
||||
var oldValue = this.map[name]
|
||||
this.map[name] = oldValue ? oldValue + ', ' + value : value
|
||||
}
|
||||
|
||||
Headers.prototype['delete'] = function(name) {
|
||||
delete this.map[normalizeName(name)]
|
||||
}
|
||||
|
||||
Headers.prototype.get = function(name) {
|
||||
name = normalizeName(name)
|
||||
return this.has(name) ? this.map[name] : null
|
||||
}
|
||||
|
||||
Headers.prototype.has = function(name) {
|
||||
return this.map.hasOwnProperty(normalizeName(name))
|
||||
}
|
||||
|
||||
Headers.prototype.set = function(name, value) {
|
||||
this.map[normalizeName(name)] = normalizeValue(value)
|
||||
}
|
||||
|
||||
Headers.prototype.forEach = function(callback, thisArg) {
|
||||
for (var name in this.map) {
|
||||
if (this.map.hasOwnProperty(name)) {
|
||||
callback.call(thisArg, this.map[name], name, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Headers.prototype.keys = function() {
|
||||
var items = []
|
||||
this.forEach(function(value, name) {
|
||||
items.push(name)
|
||||
})
|
||||
return iteratorFor(items)
|
||||
}
|
||||
|
||||
Headers.prototype.values = function() {
|
||||
var items = []
|
||||
this.forEach(function(value) {
|
||||
items.push(value)
|
||||
})
|
||||
return iteratorFor(items)
|
||||
}
|
||||
|
||||
Headers.prototype.entries = function() {
|
||||
var items = []
|
||||
this.forEach(function(value, name) {
|
||||
items.push([name, value])
|
||||
})
|
||||
return iteratorFor(items)
|
||||
}
|
||||
|
||||
if (support.iterable) {
|
||||
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
|
||||
}
|
||||
|
||||
function consumed(body) {
|
||||
if (body._noBody) return
|
||||
if (body.bodyUsed) {
|
||||
return Promise.reject(new TypeError('Already read'))
|
||||
}
|
||||
body.bodyUsed = true
|
||||
}
|
||||
|
||||
function fileReaderReady(reader) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
reader.onload = function() {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.onerror = function() {
|
||||
reject(reader.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readBlobAsArrayBuffer(blob) {
|
||||
var reader = new FileReader()
|
||||
var promise = fileReaderReady(reader)
|
||||
reader.readAsArrayBuffer(blob)
|
||||
return promise
|
||||
}
|
||||
|
||||
function readBlobAsText(blob) {
|
||||
var reader = new FileReader()
|
||||
var promise = fileReaderReady(reader)
|
||||
var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type)
|
||||
var encoding = match ? match[1] : 'utf-8'
|
||||
reader.readAsText(blob, encoding)
|
||||
return promise
|
||||
}
|
||||
|
||||
function readArrayBufferAsText(buf) {
|
||||
var view = new Uint8Array(buf)
|
||||
var chars = new Array(view.length)
|
||||
|
||||
for (var i = 0; i < view.length; i++) {
|
||||
chars[i] = String.fromCharCode(view[i])
|
||||
}
|
||||
return chars.join('')
|
||||
}
|
||||
|
||||
function bufferClone(buf) {
|
||||
if (buf.slice) {
|
||||
return buf.slice(0)
|
||||
} else {
|
||||
var view = new Uint8Array(buf.byteLength)
|
||||
view.set(new Uint8Array(buf))
|
||||
return view.buffer
|
||||
}
|
||||
}
|
||||
|
||||
function Body() {
|
||||
this.bodyUsed = false
|
||||
|
||||
this._initBody = function(body) {
|
||||
/*
|
||||
fetch-mock wraps the Response object in an ES6 Proxy to
|
||||
provide useful test harness features such as flush. However, on
|
||||
ES5 browsers without fetch or Proxy support pollyfills must be used;
|
||||
the proxy-pollyfill is unable to proxy an attribute unless it exists
|
||||
on the object before the Proxy is created. This change ensures
|
||||
Response.bodyUsed exists on the instance, while maintaining the
|
||||
semantic of setting Request.bodyUsed in the constructor before
|
||||
_initBody is called.
|
||||
*/
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this.bodyUsed = this.bodyUsed
|
||||
this._bodyInit = body
|
||||
if (!body) {
|
||||
this._noBody = true;
|
||||
this._bodyText = ''
|
||||
} else if (typeof body === 'string') {
|
||||
this._bodyText = body
|
||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
||||
this._bodyBlob = body
|
||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
||||
this._bodyFormData = body
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this._bodyText = body.toString()
|
||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
||||
this._bodyArrayBuffer = bufferClone(body.buffer)
|
||||
// IE 10-11 can't handle a DataView body.
|
||||
this._bodyInit = new Blob([this._bodyArrayBuffer])
|
||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
||||
this._bodyArrayBuffer = bufferClone(body)
|
||||
} else {
|
||||
this._bodyText = body = Object.prototype.toString.call(body)
|
||||
}
|
||||
|
||||
if (!this.headers.get('content-type')) {
|
||||
if (typeof body === 'string') {
|
||||
this.headers.set('content-type', 'text/plain;charset=UTF-8')
|
||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
||||
this.headers.set('content-type', this._bodyBlob.type)
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (support.blob) {
|
||||
this.blob = function() {
|
||||
var rejected = consumed(this)
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return Promise.resolve(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as blob')
|
||||
} else {
|
||||
return Promise.resolve(new Blob([this._bodyText]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.arrayBuffer = function() {
|
||||
if (this._bodyArrayBuffer) {
|
||||
var isConsumed = consumed(this)
|
||||
if (isConsumed) {
|
||||
return isConsumed
|
||||
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
||||
return Promise.resolve(
|
||||
this._bodyArrayBuffer.buffer.slice(
|
||||
this._bodyArrayBuffer.byteOffset,
|
||||
this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return Promise.resolve(this._bodyArrayBuffer)
|
||||
}
|
||||
} else if (support.blob) {
|
||||
return this.blob().then(readBlobAsArrayBuffer)
|
||||
} else {
|
||||
throw new Error('could not read as ArrayBuffer')
|
||||
}
|
||||
}
|
||||
|
||||
this.text = function() {
|
||||
var rejected = consumed(this)
|
||||
if (rejected) {
|
||||
return rejected
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return readBlobAsText(this._bodyBlob)
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as text')
|
||||
} else {
|
||||
return Promise.resolve(this._bodyText)
|
||||
}
|
||||
}
|
||||
|
||||
if (support.formData) {
|
||||
this.formData = function() {
|
||||
return this.text().then(decode)
|
||||
}
|
||||
}
|
||||
|
||||
this.json = function() {
|
||||
return this.text().then(JSON.parse)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// HTTP methods whose capitalization should be normalized
|
||||
var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']
|
||||
|
||||
function normalizeMethod(method) {
|
||||
var upcased = method.toUpperCase()
|
||||
return methods.indexOf(upcased) > -1 ? upcased : method
|
||||
}
|
||||
|
||||
export function Request(input, options) {
|
||||
if (!(this instanceof Request)) {
|
||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
||||
}
|
||||
|
||||
options = options || {}
|
||||
var body = options.body
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (input.bodyUsed) {
|
||||
throw new TypeError('Already read')
|
||||
}
|
||||
this.url = input.url
|
||||
this.credentials = input.credentials
|
||||
if (!options.headers) {
|
||||
this.headers = new Headers(input.headers)
|
||||
}
|
||||
this.method = input.method
|
||||
this.mode = input.mode
|
||||
this.signal = input.signal
|
||||
if (!body && input._bodyInit != null) {
|
||||
body = input._bodyInit
|
||||
input.bodyUsed = true
|
||||
}
|
||||
} else {
|
||||
this.url = String(input)
|
||||
}
|
||||
|
||||
this.credentials = options.credentials || this.credentials || 'same-origin'
|
||||
if (options.headers || !this.headers) {
|
||||
this.headers = new Headers(options.headers)
|
||||
}
|
||||
this.method = normalizeMethod(options.method || this.method || 'GET')
|
||||
this.mode = options.mode || this.mode || null
|
||||
this.signal = options.signal || this.signal || (function () {
|
||||
if ('AbortController' in g) {
|
||||
var ctrl = new AbortController();
|
||||
return ctrl.signal;
|
||||
}
|
||||
}());
|
||||
this.referrer = null
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests')
|
||||
}
|
||||
this._initBody(body)
|
||||
|
||||
if (this.method === 'GET' || this.method === 'HEAD') {
|
||||
if (options.cache === 'no-store' || options.cache === 'no-cache') {
|
||||
// Search for a '_' parameter in the query string
|
||||
var reParamSearch = /([?&])_=[^&]*/
|
||||
if (reParamSearch.test(this.url)) {
|
||||
// If it already exists then set the value with the current time
|
||||
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime())
|
||||
} else {
|
||||
// Otherwise add a new '_' parameter to the end with the current time
|
||||
var reQueryString = /\?/
|
||||
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Request.prototype.clone = function() {
|
||||
return new Request(this, {body: this._bodyInit})
|
||||
}
|
||||
|
||||
function decode(body) {
|
||||
var form = new FormData()
|
||||
body
|
||||
.trim()
|
||||
.split('&')
|
||||
.forEach(function(bytes) {
|
||||
if (bytes) {
|
||||
var split = bytes.split('=')
|
||||
var name = split.shift().replace(/\+/g, ' ')
|
||||
var value = split.join('=').replace(/\+/g, ' ')
|
||||
form.append(decodeURIComponent(name), decodeURIComponent(value))
|
||||
}
|
||||
})
|
||||
return form
|
||||
}
|
||||
|
||||
function parseHeaders(rawHeaders) {
|
||||
var headers = new Headers()
|
||||
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.2
|
||||
var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ')
|
||||
// Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill
|
||||
// https://github.com/github/fetch/issues/748
|
||||
// https://github.com/zloirock/core-js/issues/751
|
||||
preProcessedHeaders
|
||||
.split('\r')
|
||||
.map(function(header) {
|
||||
return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header
|
||||
})
|
||||
.forEach(function(line) {
|
||||
var parts = line.split(':')
|
||||
var key = parts.shift().trim()
|
||||
if (key) {
|
||||
var value = parts.join(':').trim()
|
||||
try {
|
||||
headers.append(key, value)
|
||||
} catch (error) {
|
||||
console.warn('Response ' + error.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
return headers
|
||||
}
|
||||
|
||||
Body.call(Request.prototype)
|
||||
|
||||
export function Response(bodyInit, options) {
|
||||
if (!(this instanceof Response)) {
|
||||
throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.')
|
||||
}
|
||||
if (!options) {
|
||||
options = {}
|
||||
}
|
||||
|
||||
this.type = 'default'
|
||||
this.status = options.status === undefined ? 200 : options.status
|
||||
if (this.status < 200 || this.status > 599) {
|
||||
throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].")
|
||||
}
|
||||
this.ok = this.status >= 200 && this.status < 300
|
||||
this.statusText = options.statusText === undefined ? '' : '' + options.statusText
|
||||
this.headers = new Headers(options.headers)
|
||||
this.url = options.url || ''
|
||||
this._initBody(bodyInit)
|
||||
}
|
||||
|
||||
Body.call(Response.prototype)
|
||||
|
||||
Response.prototype.clone = function() {
|
||||
return new Response(this._bodyInit, {
|
||||
status: this.status,
|
||||
statusText: this.statusText,
|
||||
headers: new Headers(this.headers),
|
||||
url: this.url
|
||||
})
|
||||
}
|
||||
|
||||
Response.error = function() {
|
||||
var response = new Response(null, {status: 200, statusText: ''})
|
||||
response.ok = false
|
||||
response.status = 0
|
||||
response.type = 'error'
|
||||
return response
|
||||
}
|
||||
|
||||
var redirectStatuses = [301, 302, 303, 307, 308]
|
||||
|
||||
Response.redirect = function(url, status) {
|
||||
if (redirectStatuses.indexOf(status) === -1) {
|
||||
throw new RangeError('Invalid status code')
|
||||
}
|
||||
|
||||
return new Response(null, {status: status, headers: {location: url}})
|
||||
}
|
||||
|
||||
export var DOMException = g.DOMException
|
||||
try {
|
||||
new DOMException()
|
||||
} catch (err) {
|
||||
DOMException = function(message, name) {
|
||||
this.message = message
|
||||
this.name = name
|
||||
var error = Error(message)
|
||||
this.stack = error.stack
|
||||
}
|
||||
DOMException.prototype = Object.create(Error.prototype)
|
||||
DOMException.prototype.constructor = DOMException
|
||||
}
|
||||
|
||||
export function fetch(input, init) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = new Request(input, init)
|
||||
|
||||
if (request.signal && request.signal.aborted) {
|
||||
return reject(new DOMException('Aborted', 'AbortError'))
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
|
||||
function abortXhr() {
|
||||
xhr.abort()
|
||||
}
|
||||
|
||||
xhr.onload = function() {
|
||||
var options = {
|
||||
statusText: xhr.statusText,
|
||||
headers: parseHeaders(xhr.getAllResponseHeaders() || '')
|
||||
}
|
||||
// This check if specifically for when a user fetches a file locally from the file system
|
||||
// Only if the status is out of a normal range
|
||||
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
|
||||
options.status = 200;
|
||||
} else {
|
||||
options.status = xhr.status;
|
||||
}
|
||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
|
||||
var body = 'response' in xhr ? xhr.response : xhr.responseText
|
||||
setTimeout(function() {
|
||||
resolve(new Response(body, options))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
xhr.onerror = function() {
|
||||
setTimeout(function() {
|
||||
reject(new TypeError('Network request failed'))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
setTimeout(function() {
|
||||
reject(new TypeError('Network request timed out'))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
xhr.onabort = function() {
|
||||
setTimeout(function() {
|
||||
reject(new DOMException('Aborted', 'AbortError'))
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function fixUrl(url) {
|
||||
try {
|
||||
return url === '' && g.location.href ? g.location.href : url
|
||||
} catch (e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
xhr.open(request.method, fixUrl(request.url), true)
|
||||
|
||||
if (request.credentials === 'include') {
|
||||
xhr.withCredentials = true
|
||||
} else if (request.credentials === 'omit') {
|
||||
xhr.withCredentials = false
|
||||
}
|
||||
|
||||
if ('responseType' in xhr) {
|
||||
if (support.blob) {
|
||||
xhr.responseType = 'blob'
|
||||
} else if (
|
||||
support.arrayBuffer
|
||||
) {
|
||||
xhr.responseType = 'arraybuffer'
|
||||
}
|
||||
}
|
||||
|
||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {
|
||||
var names = [];
|
||||
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
|
||||
names.push(normalizeName(name))
|
||||
xhr.setRequestHeader(name, normalizeValue(init.headers[name]))
|
||||
})
|
||||
request.headers.forEach(function(value, name) {
|
||||
if (names.indexOf(name) === -1) {
|
||||
xhr.setRequestHeader(name, value)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
request.headers.forEach(function(value, name) {
|
||||
xhr.setRequestHeader(name, value)
|
||||
})
|
||||
}
|
||||
|
||||
if (request.signal) {
|
||||
request.signal.addEventListener('abort', abortXhr)
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
// DONE (success or failure)
|
||||
if (xhr.readyState === 4) {
|
||||
request.signal.removeEventListener('abort', abortXhr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
|
||||
})
|
||||
}
|
||||
|
||||
fetch.polyfill = true
|
||||
|
||||
if (!g.fetch) {
|
||||
g.fetch = fetch
|
||||
g.Headers = Headers
|
||||
g.Request = Request
|
||||
g.Response = Response
|
||||
}
|
||||
620
packages/core/fetch/index.ts
Normal file
620
packages/core/fetch/index.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
const g: any = (typeof globalThis !== 'undefined' && globalThis) || (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global) || {};
|
||||
|
||||
// Feature support detection
|
||||
interface Support {
|
||||
searchParams: boolean;
|
||||
iterable: boolean;
|
||||
blob: boolean;
|
||||
formData: boolean;
|
||||
arrayBuffer: boolean;
|
||||
}
|
||||
const support: Support = {
|
||||
searchParams: 'URLSearchParams' in g,
|
||||
iterable: 'Symbol' in g && 'iterator' in Symbol,
|
||||
blob:
|
||||
'FileReader' in g &&
|
||||
'Blob' in g &&
|
||||
(() => {
|
||||
try {
|
||||
new Blob();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})(),
|
||||
formData: 'FormData' in g,
|
||||
arrayBuffer: 'ArrayBuffer' in g,
|
||||
};
|
||||
|
||||
function isDataView(obj: any): obj is DataView {
|
||||
return obj && DataView.prototype.isPrototypeOf(obj);
|
||||
}
|
||||
|
||||
let isArrayBufferView: (obj: any) => boolean;
|
||||
if (support.arrayBuffer) {
|
||||
const viewClasses = ['[object Int8Array]', '[object Uint8Array]', '[object Uint8ClampedArray]', '[object Int16Array]', '[object Uint16Array]', '[object Int32Array]', '[object Uint32Array]', '[object Float32Array]', '[object Float64Array]'];
|
||||
|
||||
isArrayBufferView =
|
||||
ArrayBuffer.isView ||
|
||||
((obj: any) => {
|
||||
return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeName(name: any): string {
|
||||
if (typeof name !== 'string') {
|
||||
name = String(name);
|
||||
}
|
||||
if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {
|
||||
throw new TypeError('Invalid character in header field name: "' + name + '"');
|
||||
}
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeValue(value: any): string {
|
||||
if (typeof value !== 'string') {
|
||||
value = String(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Build a destructive iterator for the value list
|
||||
function iteratorFor(items: any[]): IterableIterator<any> {
|
||||
const iterator: any = {
|
||||
next: () => {
|
||||
const value = items.shift();
|
||||
return { done: value === undefined, value };
|
||||
},
|
||||
};
|
||||
|
||||
if (support.iterable) {
|
||||
(iterator as any)[Symbol.iterator] = () => iterator;
|
||||
}
|
||||
|
||||
return iterator;
|
||||
}
|
||||
|
||||
export type HeaderInit = Headers | string[][] | Record<string, string>;
|
||||
|
||||
export class Headers {
|
||||
private map: Record<string, string> = {};
|
||||
|
||||
constructor(headers?: HeaderInit) {
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, name) => {
|
||||
this.append(name, value);
|
||||
});
|
||||
} else if (Array.isArray(headers)) {
|
||||
headers.forEach((header) => {
|
||||
if (header.length !== 2) {
|
||||
throw new TypeError('Headers constructor: expected name/value pair to be length 2, found ' + header.length);
|
||||
}
|
||||
this.append(header[0], header[1]);
|
||||
});
|
||||
} else if (headers) {
|
||||
Object.getOwnPropertyNames(headers).forEach((name) => {
|
||||
this.append(name, (headers as Record<string, string>)[name]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
append(name: string, value: string): void {
|
||||
name = normalizeName(name);
|
||||
value = normalizeValue(value);
|
||||
const oldValue = this.map[name];
|
||||
this.map[name] = oldValue ? oldValue + ', ' + value : value;
|
||||
}
|
||||
|
||||
delete(name: string): void {
|
||||
delete this.map[normalizeName(name)];
|
||||
}
|
||||
|
||||
get(name: string): string | null {
|
||||
name = normalizeName(name);
|
||||
return this.has(name) ? this.map[name] : null;
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(this.map, normalizeName(name));
|
||||
}
|
||||
|
||||
set(name: string, value: string): void {
|
||||
this.map[normalizeName(name)] = normalizeValue(value);
|
||||
}
|
||||
|
||||
forEach(callback: (value: string, name: string, headers: Headers) => void, thisArg?: any): void {
|
||||
for (const name in this.map) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.map, name)) {
|
||||
callback.call(thisArg, this.map[name], name, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
keys(): IterableIterator<string> {
|
||||
const items: string[] = [];
|
||||
this.forEach((_, name) => items.push(name));
|
||||
return iteratorFor(items);
|
||||
}
|
||||
|
||||
values(): IterableIterator<string> {
|
||||
const items: string[] = [];
|
||||
this.forEach((value) => items.push(value));
|
||||
return iteratorFor(items);
|
||||
}
|
||||
|
||||
entries(): IterableIterator<[string, string]> {
|
||||
const items: [string, string][] = [];
|
||||
this.forEach((value, name) => items.push([name, value]));
|
||||
return iteratorFor(items);
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[string, string]> {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
|
||||
// Body mixin
|
||||
export class Body {
|
||||
bodyUsed = false;
|
||||
_bodyInit: any;
|
||||
protected _bodyText?: string;
|
||||
protected _bodyBlob?: Blob;
|
||||
protected _bodyFormData?: FormData;
|
||||
protected _bodyArrayBuffer?: ArrayBuffer;
|
||||
protected _noBody?: boolean;
|
||||
protected headers!: Headers;
|
||||
|
||||
protected _initBody(body: any): void {
|
||||
// Ensure bodyUsed property exists
|
||||
this.bodyUsed = this.bodyUsed;
|
||||
this._bodyInit = body;
|
||||
|
||||
if (!body) {
|
||||
this._noBody = true;
|
||||
this._bodyText = '';
|
||||
} else if (typeof body === 'string') {
|
||||
this._bodyText = body;
|
||||
} else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
|
||||
this._bodyBlob = body as Blob;
|
||||
} else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
|
||||
this._bodyFormData = body as FormData;
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this._bodyText = body.toString();
|
||||
} else if (support.arrayBuffer && support.blob && isDataView(body)) {
|
||||
// @ts-ignore
|
||||
this._bodyArrayBuffer = bufferClone((body as DataView).buffer);
|
||||
this._bodyInit = new Blob([this._bodyArrayBuffer]);
|
||||
} else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
|
||||
this._bodyArrayBuffer = bufferClone(body as ArrayBuffer);
|
||||
} else {
|
||||
this._bodyText = body = Object.prototype.toString.call(body);
|
||||
}
|
||||
|
||||
// Set Content-Type header if not set
|
||||
if (!this.headers.get('content-type')) {
|
||||
if (typeof body === 'string') {
|
||||
this.headers.set('content-type', 'text/plain;charset=UTF-8');
|
||||
} else if (this._bodyBlob && this._bodyBlob.type) {
|
||||
this.headers.set('content-type', this._bodyBlob.type);
|
||||
} else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
|
||||
this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blob?(): Promise<Blob> {
|
||||
const rejected = consumed(this);
|
||||
if (rejected) {
|
||||
return rejected as Promise<any>;
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return Promise.resolve(this._bodyBlob);
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(new Blob([this._bodyArrayBuffer]));
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as blob');
|
||||
} else {
|
||||
return Promise.resolve(new Blob([this._bodyText!]));
|
||||
}
|
||||
}
|
||||
|
||||
arrayBuffer(): Promise<ArrayBuffer> {
|
||||
if (this._bodyArrayBuffer) {
|
||||
const consumedResult = consumed(this);
|
||||
if (consumedResult) {
|
||||
return consumedResult as Promise<any>;
|
||||
} else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {
|
||||
return Promise.resolve((this._bodyArrayBuffer as any).buffer.slice((this._bodyArrayBuffer as any).byteOffset, (this._bodyArrayBuffer as any).byteOffset + (this._bodyArrayBuffer as any).byteLength));
|
||||
} else {
|
||||
return Promise.resolve(this._bodyArrayBuffer);
|
||||
}
|
||||
} else if (support.blob) {
|
||||
// @ts-ignore
|
||||
return this.blob!().then(readBlobAsArrayBuffer);
|
||||
} else {
|
||||
throw new Error('could not read as ArrayBuffer');
|
||||
}
|
||||
}
|
||||
|
||||
text(): Promise<string> {
|
||||
const rejected = consumed(this);
|
||||
if (rejected) {
|
||||
return rejected as Promise<any>;
|
||||
}
|
||||
|
||||
if (this._bodyBlob) {
|
||||
return readBlobAsText(this._bodyBlob);
|
||||
} else if (this._bodyArrayBuffer) {
|
||||
return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer));
|
||||
} else if (this._bodyFormData) {
|
||||
throw new Error('could not read FormData body as text');
|
||||
} else {
|
||||
return Promise.resolve(this._bodyText!);
|
||||
}
|
||||
}
|
||||
|
||||
formData?(): Promise<FormData> {
|
||||
return this.text().then(decode);
|
||||
}
|
||||
|
||||
json(): Promise<any> {
|
||||
return this.text().then(JSON.parse);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for Body
|
||||
function consumed(body: any): Promise<any> | undefined {
|
||||
if (body._noBody) return;
|
||||
if (body.bodyUsed) {
|
||||
return Promise.reject(new TypeError('Already read'));
|
||||
}
|
||||
body.bodyUsed = true;
|
||||
}
|
||||
|
||||
function fileReaderReady(reader: FileReader): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
});
|
||||
}
|
||||
|
||||
function readBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer | string | null> {
|
||||
const reader = new FileReader();
|
||||
const promise = fileReaderReady(reader);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function readBlobAsText(blob: Blob): Promise<string | null> {
|
||||
const reader = new FileReader();
|
||||
const promise = fileReaderReady(reader);
|
||||
const match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type);
|
||||
const encoding = match ? match[1] : 'utf-8';
|
||||
reader.readAsText(blob, encoding);
|
||||
return promise;
|
||||
}
|
||||
|
||||
function readArrayBufferAsText(buf: ArrayBuffer): string {
|
||||
const view = new Uint8Array(buf);
|
||||
const chars = new Array(view.length);
|
||||
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
chars[i] = String.fromCharCode(view[i]);
|
||||
}
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
function bufferClone(buf: ArrayBuffer): ArrayBuffer {
|
||||
if (buf.slice) {
|
||||
return buf.slice(0);
|
||||
} else {
|
||||
const view = new Uint8Array(buf.byteLength);
|
||||
view.set(new Uint8Array(buf));
|
||||
return view.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP methods whose capitalization should be normalized
|
||||
const methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE'];
|
||||
|
||||
function normalizeMethod(method: string): string {
|
||||
const upcased = method.toUpperCase();
|
||||
return methods.indexOf(upcased) > -1 ? upcased : method;
|
||||
}
|
||||
|
||||
// Request class
|
||||
export type RequestInfo = string | Request;
|
||||
export interface RequestInit {
|
||||
method?: string;
|
||||
headers?: HeaderInit;
|
||||
body?: any;
|
||||
mode?: string | null;
|
||||
credentials?: RequestCredentials;
|
||||
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached';
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export class Request extends Body {
|
||||
url: string;
|
||||
credentials: RequestCredentials;
|
||||
headers: Headers;
|
||||
method: string;
|
||||
mode: string | null;
|
||||
signal?: AbortSignal;
|
||||
referrer: string | null;
|
||||
|
||||
constructor(input: RequestInfo, options: RequestInit = {}) {
|
||||
super();
|
||||
let body = options.body;
|
||||
|
||||
if (input instanceof Request) {
|
||||
if (input.bodyUsed) {
|
||||
throw new TypeError('Already read');
|
||||
}
|
||||
this.url = input.url;
|
||||
this.credentials = input.credentials;
|
||||
if (!options.headers) {
|
||||
this.headers = new Headers(input.headers);
|
||||
}
|
||||
this.method = input.method;
|
||||
this.mode = input.mode;
|
||||
this.signal = input.signal;
|
||||
if (!body && input._bodyInit != null) {
|
||||
body = input._bodyInit;
|
||||
input.bodyUsed = true;
|
||||
}
|
||||
} else {
|
||||
this.url = String(input);
|
||||
}
|
||||
|
||||
this.credentials = options.credentials || this.credentials || 'same-origin';
|
||||
if (options.headers || !this.headers) {
|
||||
this.headers = new Headers(options.headers);
|
||||
}
|
||||
this.method = normalizeMethod(options.method || this.method || 'GET');
|
||||
this.mode = options.mode || this.mode || null;
|
||||
this.signal = options.signal || this.signal || ('AbortController' in g ? new AbortController().signal : undefined);
|
||||
this.referrer = null;
|
||||
|
||||
if ((this.method === 'GET' || this.method === 'HEAD') && body) {
|
||||
throw new TypeError('Body not allowed for GET or HEAD requests');
|
||||
}
|
||||
this._initBody(body);
|
||||
|
||||
if (this.method === 'GET' || this.method === 'HEAD') {
|
||||
if (options.cache === 'no-store' || options.cache === 'no-cache') {
|
||||
const reParamSearch = /([?&])_=[^&]*/;
|
||||
if (reParamSearch.test(this.url)) {
|
||||
this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime());
|
||||
} else {
|
||||
const reQueryString = /\?/;
|
||||
this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clone(): Request {
|
||||
return new Request(this, { body: this._bodyInit });
|
||||
}
|
||||
}
|
||||
|
||||
// Decode URL-encoded form data
|
||||
function decode(body: string): FormData {
|
||||
const form = new FormData();
|
||||
body
|
||||
.trim()
|
||||
.split('&')
|
||||
.forEach((bytes) => {
|
||||
if (bytes) {
|
||||
const split = bytes.split('=');
|
||||
const name = split.shift()!.replace(/\+/g, ' ');
|
||||
const value = split.join('=').replace(/\+/g, ' ');
|
||||
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
||||
}
|
||||
});
|
||||
return form;
|
||||
}
|
||||
|
||||
// Parse raw headers string into Headers
|
||||
function parseHeaders(rawHeaders: string): Headers {
|
||||
const headers = new Headers();
|
||||
const preProcessed = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
|
||||
preProcessed
|
||||
.split('\r')
|
||||
.map((header) => (header.indexOf('\n') === 0 ? header.substr(1) : header))
|
||||
.forEach((line) => {
|
||||
const parts = line.split(':');
|
||||
const key = parts.shift()!.trim();
|
||||
if (key) {
|
||||
const value = parts.join(':').trim();
|
||||
try {
|
||||
headers.append(key, value);
|
||||
} catch (err) {
|
||||
console.warn('Response ' + (err as Error).message);
|
||||
}
|
||||
}
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Response class
|
||||
export interface ResponseInit {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
headers?: HeaderInit;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const redirectStatuses = [301, 302, 303, 307, 308];
|
||||
|
||||
export class Response extends Body {
|
||||
type: string;
|
||||
status: number;
|
||||
ok: boolean;
|
||||
statusText: string;
|
||||
headers: Headers;
|
||||
url: string;
|
||||
|
||||
constructor(bodyInit: any, options: ResponseInit = {}) {
|
||||
super();
|
||||
this.type = 'default';
|
||||
this.status = options.status === undefined ? 200 : options.status!;
|
||||
if (this.status < 200 || this.status > 599) {
|
||||
throw new RangeError(`Failed to construct 'Response': The status provided (${this.status}) is outside the range [200, 599].`);
|
||||
}
|
||||
this.ok = this.status >= 200 && this.status < 300;
|
||||
this.statusText = options.statusText === undefined ? '' : String(options.statusText);
|
||||
this.headers = new Headers(options.headers);
|
||||
this.url = options.url || '';
|
||||
this._initBody(bodyInit);
|
||||
}
|
||||
|
||||
clone(): Response {
|
||||
return new Response(this._bodyInit, {
|
||||
status: this.status,
|
||||
statusText: this.statusText,
|
||||
headers: new Headers(this.headers),
|
||||
url: this.url,
|
||||
});
|
||||
}
|
||||
|
||||
static error(): Response {
|
||||
const response = new Response(null, { status: 200, statusText: '' });
|
||||
response.ok = false;
|
||||
response.status = 0;
|
||||
response.type = 'error';
|
||||
return response;
|
||||
}
|
||||
|
||||
static redirect(url: string, status: number): Response {
|
||||
if (!redirectStatuses.includes(status)) {
|
||||
throw new RangeError('Invalid status code');
|
||||
}
|
||||
return new Response(null, { status, headers: { location: url } });
|
||||
}
|
||||
}
|
||||
|
||||
// DOMException polyfill
|
||||
export let DOMException: any = g.DOMException;
|
||||
try {
|
||||
new (g.DOMException as any)();
|
||||
} catch (err) {
|
||||
DOMException = class {
|
||||
message: string;
|
||||
name: string;
|
||||
stack?: string;
|
||||
constructor(message: string, name: string) {
|
||||
this.message = message;
|
||||
this.name = name;
|
||||
const error = new Error(message);
|
||||
this.stack = error.stack;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// fetch function
|
||||
export function fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new Request(input, init as any);
|
||||
|
||||
if (request.signal && (request.signal as any).aborted) {
|
||||
return reject(new DOMException('Aborted', 'AbortError'));
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
function abortXhr() {
|
||||
xhr.abort();
|
||||
}
|
||||
|
||||
xhr.onload = function () {
|
||||
const options: any = {
|
||||
statusText: xhr.statusText,
|
||||
headers: parseHeaders(xhr.getAllResponseHeaders() || ''),
|
||||
};
|
||||
// Local file handling
|
||||
if (request.url.startsWith('file://') && (xhr.status < 200 || xhr.status > 599)) {
|
||||
options.status = 200;
|
||||
} else {
|
||||
options.status = xhr.status;
|
||||
}
|
||||
options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
|
||||
// @ts-ignore
|
||||
const body = 'response' in xhr ? xhr.response : xhr.responseText;
|
||||
setTimeout(() => resolve(new Response(body, options)), 0);
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
setTimeout(() => reject(new TypeError('Network request failed')), 0);
|
||||
};
|
||||
|
||||
xhr.ontimeout = function () {
|
||||
setTimeout(() => reject(new TypeError('Network request timed out')), 0);
|
||||
};
|
||||
|
||||
xhr.onabort = function () {
|
||||
setTimeout(() => reject(new DOMException('Aborted', 'AbortError')), 0);
|
||||
};
|
||||
|
||||
function fixUrl(url: string): string {
|
||||
try {
|
||||
return url === '' && g.location.href ? g.location.href : url;
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
xhr.open(request.method, fixUrl(request.url), true);
|
||||
|
||||
if (request.credentials === 'include') {
|
||||
xhr.withCredentials = true;
|
||||
} else if (request.credentials === 'omit') {
|
||||
xhr.withCredentials = false;
|
||||
}
|
||||
|
||||
if ('responseType' in xhr) {
|
||||
if (support.blob) {
|
||||
(xhr as any).responseType = 'blob';
|
||||
} else if (support.arrayBuffer) {
|
||||
(xhr as any).responseType = 'arraybuffer';
|
||||
}
|
||||
}
|
||||
|
||||
if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers) && !(g.Headers && init.headers instanceof g.Headers)) {
|
||||
const names: string[] = [];
|
||||
Object.getOwnPropertyNames(init.headers).forEach((name) => {
|
||||
names.push(normalizeName(name));
|
||||
xhr.setRequestHeader(name, normalizeValue((init.headers as any)[name]));
|
||||
});
|
||||
request.headers.forEach((value, name) => {
|
||||
if (!names.includes(name)) {
|
||||
xhr.setRequestHeader(name, value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
request.headers.forEach((value, name) => xhr.setRequestHeader(name, value));
|
||||
}
|
||||
|
||||
if (request.signal) {
|
||||
request.signal.addEventListener('abort', abortXhr);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
(request.signal as any).removeEventListener('abort', abortXhr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
|
||||
});
|
||||
}
|
||||
|
||||
// Attach polyfill to globals
|
||||
(fetch as any).polyfill = true;
|
||||
if (!g.fetch) {
|
||||
g.fetch = fetch;
|
||||
g.Headers = Headers;
|
||||
g.Request = Request;
|
||||
g.Response = Response;
|
||||
}
|
||||
@@ -20,9 +20,9 @@
|
||||
"{workspaceRoot}/LICENSE",
|
||||
"{projectRoot}/README.md",
|
||||
"{projectRoot}/global-types.d.ts",
|
||||
"{projectRoot}/fetch/LICENSE",
|
||||
{ "glob": "**/*", "input": "{projectRoot}/js-libs/", "output": "./js-libs/" },
|
||||
{ "glob": "**/*", "input": "{projectRoot}/cli-hooks/", "output": "./cli-hooks/" },
|
||||
{ "glob": "**/*", "input": "{projectRoot}/fetch/", "output": "./fetch/" },
|
||||
{ "glob": "**/*", "input": "{projectRoot}/css/", "output": "./css/" },
|
||||
{ "glob": "**/*", "input": "{projectRoot}/css-value/", "output": "./css-value/" },
|
||||
{ "glob": "**/*", "input": "{projectRoot}/platforms/", "output": "./platforms/" },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AndroidActionItemSettings, AndroidActionBarSettings as AndroidActionBarSettingsDefinition, ActionItem as ActionItemDefinition } from '.';
|
||||
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../application';
|
||||
import { ActionItemBase, ActionBarBase, isVisible, flatProperty, traceMissingIcon, androidContentInsetLeftProperty, androidContentInsetRightProperty } from './action-bar-common';
|
||||
import { AndroidHelper, View } from '../core/view';
|
||||
import { Color } from '../../color';
|
||||
@@ -6,7 +7,6 @@ import { layout, RESOURCE_PREFIX, isFontIconURI } from '../../utils';
|
||||
import { colorProperty } from '../styling/style-properties';
|
||||
import { ImageSource } from '../../image-source';
|
||||
import { Application } from '../../application';
|
||||
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../accessibility';
|
||||
import type { Background } from '../styling/background';
|
||||
import { SDK_VERSION } from '../../utils/constants';
|
||||
import { NativeScriptAndroidView } from '../utils';
|
||||
|
||||
@@ -19,7 +19,6 @@ import { profile } from '../../../profiling';
|
||||
|
||||
import { DOMNode } from '../../../debugger/dom-types';
|
||||
import { applyInlineStyle, CssState, StyleScope } from '../../styling/style-scope';
|
||||
import { ViewBase as ViewBaseDefinition } from '.';
|
||||
import { booleanConverter } from './utils';
|
||||
|
||||
export { booleanConverter } from './utils';
|
||||
@@ -106,12 +105,12 @@ export interface ShowModalOptions {
|
||||
* @param criterion - The type of ancestor view we are looking for. Could be a string containing a class name or an actual type.
|
||||
* Returns an instance of a view (if found), otherwise undefined.
|
||||
*/
|
||||
export function getAncestor(view: ViewBaseDefinition, criterion: string | { new () }): ViewBaseDefinition {
|
||||
let matcher: (view: ViewBaseDefinition) => boolean = null;
|
||||
export function getAncestor(view: ViewBase, criterion: string | { new () }): ViewBase {
|
||||
let matcher: (view: ViewBase) => boolean = null;
|
||||
if (typeof criterion === 'string') {
|
||||
matcher = (view: ViewBaseDefinition) => view.typeName === criterion;
|
||||
matcher = (view: ViewBase) => view.typeName === criterion;
|
||||
} else {
|
||||
matcher = (view: ViewBaseDefinition) => view instanceof criterion;
|
||||
matcher = (view: ViewBase) => view instanceof criterion;
|
||||
}
|
||||
|
||||
for (let parent = view.parent; parent != null; parent = parent.parent) {
|
||||
@@ -129,7 +128,7 @@ export function getAncestor(view: ViewBaseDefinition, criterion: string | { new
|
||||
* @param id - The id of the view to look for.
|
||||
* Returns an instance of a view (if found), otherwise undefined.
|
||||
*/
|
||||
export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefinition {
|
||||
export function getViewById(view: ViewBase, id: string): ViewBase {
|
||||
if (!view) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -138,8 +137,8 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
|
||||
return view;
|
||||
}
|
||||
|
||||
let retVal: ViewBaseDefinition;
|
||||
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
|
||||
let retVal: ViewBase;
|
||||
const descendantsCallback = function (child: ViewBase): boolean {
|
||||
if (child.id === id) {
|
||||
retVal = child;
|
||||
|
||||
@@ -161,7 +160,7 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
|
||||
* @param domId - The id of the view to look for.
|
||||
* Returns an instance of a view (if found), otherwise undefined.
|
||||
*/
|
||||
export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBaseDefinition {
|
||||
export function getViewByDomId(view: ViewBase, domId: number): ViewBase {
|
||||
if (!view) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -170,8 +169,8 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
|
||||
return view;
|
||||
}
|
||||
|
||||
let retVal: ViewBaseDefinition;
|
||||
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
|
||||
let retVal: ViewBase;
|
||||
const descendantsCallback = function (child: ViewBase): boolean {
|
||||
if (view._domId === domId) {
|
||||
retVal = child;
|
||||
|
||||
@@ -194,17 +193,17 @@ export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBas
|
||||
* @param selector - The selector of the view to look for.
|
||||
* Returns an instance of a view (if found), otherwise undefined.
|
||||
*/
|
||||
export function querySelectorAll(view: ViewBaseDefinition, selector: string): Array<ViewBaseDefinition> {
|
||||
export function querySelectorAll(view: ViewBase, selector: string): Array<ViewBase> {
|
||||
if (!view) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const retVal: Array<ViewBaseDefinition> = [];
|
||||
const retVal: Array<ViewBase> = [];
|
||||
if (view[selector]) {
|
||||
retVal.push(view);
|
||||
}
|
||||
|
||||
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
|
||||
const descendantsCallback = function (child: ViewBase): boolean {
|
||||
if (child[selector]) {
|
||||
retVal.push(child);
|
||||
}
|
||||
@@ -222,13 +221,13 @@ export function querySelectorAll(view: ViewBaseDefinition, selector: string): Ar
|
||||
* @param view - Starting view (parent container).
|
||||
* @param callback - A function to execute on every child. If function returns false it breaks the iteration.
|
||||
*/
|
||||
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
|
||||
export function eachDescendant(view: ViewBase, callback: (child: ViewBase) => boolean) {
|
||||
if (!callback || !view) {
|
||||
return;
|
||||
}
|
||||
|
||||
let continueIteration: boolean;
|
||||
const localCallback = function (child: ViewBaseDefinition): boolean {
|
||||
const localCallback = function (child: ViewBase): boolean {
|
||||
continueIteration = callback(child);
|
||||
if (continueIteration) {
|
||||
child.eachChild(localCallback);
|
||||
@@ -302,7 +301,7 @@ const DEFAULT_VIEW_PADDINGS: Map<string, any> = new Map();
|
||||
*
|
||||
* @nsView ViewBase
|
||||
*/
|
||||
export abstract class ViewBase extends Observable implements ViewBaseDefinition {
|
||||
export abstract class ViewBase extends Observable {
|
||||
/**
|
||||
* String value used when hooking to loaded event.
|
||||
*
|
||||
@@ -636,14 +635,14 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
/**
|
||||
* Returns the child view with the specified id.
|
||||
*/
|
||||
getViewById<T extends ViewBaseDefinition>(id: string): T {
|
||||
getViewById<T extends ViewBase>(id: string): T {
|
||||
return <T>getViewById(this, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child view with the specified domId.
|
||||
*/
|
||||
getViewByDomId<T extends ViewBaseDefinition>(domId: number): T {
|
||||
getViewByDomId<T extends ViewBase>(domId: number): T {
|
||||
return <T>getViewByDomId(this, domId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Definitions.
|
||||
import type { Point, CustomLayoutView as CustomLayoutViewDefinition, Position } from '.';
|
||||
import type { Point, Position } from './view-interfaces';
|
||||
import type { GestureTypes, GestureEventData } from '../../gestures';
|
||||
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty, AndroidHelper } from './view-common';
|
||||
@@ -17,9 +16,9 @@ import { Background, BackgroundClearFlags, refreshBorderDrawable } from '../../s
|
||||
import { profile } from '../../../profiling';
|
||||
import { topmost } from '../../frame/frame-stack';
|
||||
import { Screen } from '../../../platform';
|
||||
import { AndroidActivityBackPressedEventData, Application } from '../../../application';
|
||||
import { AndroidActivityBackPressedEventData, Application, updateAccessibilityProperties, updateContentDescription } from '../../../application';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, AccessibilityState } from '../../../accessibility';
|
||||
import * as Utils from '../../../utils';
|
||||
import { SDK_VERSION } from '../../../utils/constants';
|
||||
import { BoxShadow } from '../../styling/box-shadow';
|
||||
@@ -1276,7 +1275,7 @@ export class ContainerView extends View {
|
||||
public iosOverflowSafeArea: boolean;
|
||||
}
|
||||
|
||||
export class CustomLayoutView extends ContainerView implements CustomLayoutViewDefinition {
|
||||
export class CustomLayoutView extends ContainerView {
|
||||
nativeViewProtected: android.view.ViewGroup;
|
||||
|
||||
public createNativeView() {
|
||||
|
||||
62
packages/core/ui/core/view/index.d.ts
vendored
62
packages/core/ui/core/view/index.d.ts
vendored
@@ -9,6 +9,7 @@ import { LinearGradient } from '../../styling/linear-gradient';
|
||||
import { InheritedProperty, Property } from '../properties';
|
||||
import { ViewBase } from '../view-base';
|
||||
import { ViewCommon } from './view-common';
|
||||
import type { Point } from './view-interfaces';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
@@ -43,67 +44,6 @@ export function CSSType(type: string): ClassDecorator;
|
||||
*/
|
||||
export function viewMatchesModuleContext(view: View, context: ModuleContext, type: ModuleType[]): boolean;
|
||||
|
||||
/**
|
||||
* The Point interface describes a two dimensional location.
|
||||
* It has two properties x and y, representing the x and y coordinate of the location.
|
||||
*/
|
||||
export interface Point {
|
||||
/**
|
||||
* Represents the x coordinate of the location.
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* Represents the y coordinate of the location.
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* Represents the z coordinate of the location.
|
||||
*/
|
||||
z?: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Size interface describes abstract dimensions in two dimensional space.
|
||||
* It has two properties width and height, representing the width and height values of the size.
|
||||
*/
|
||||
export interface Size {
|
||||
/**
|
||||
* Represents the width of the size.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* Represents the height of the size.
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the data for the shownModally event.
|
||||
*/
|
||||
export interface ShownModallyData extends EventData {
|
||||
/**
|
||||
* The context (optional, may be undefined) passed to the view when shown modally.
|
||||
*/
|
||||
context?: any;
|
||||
|
||||
/**
|
||||
* A callback to call when you want to close the modally shown view.
|
||||
* Pass in any kind of arguments and you will receive when the callback parameter
|
||||
* of View.showModal is executed.
|
||||
*/
|
||||
closeCallback?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is the base class for all UI components.
|
||||
* A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Point, Position, View as ViewDefinition } from '.';
|
||||
import type { Point, Position } from './view-interfaces';
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty, testIDProperty } from './view-common';
|
||||
import { isAccessibilityServiceEnabled, updateAccessibilityProperties } from '../../../application';
|
||||
import { ShowModalOptions, hiddenProperty } from '../view-base';
|
||||
import { Trace } from '../../../trace';
|
||||
import { layout, ios as iosUtils, SDK_VERSION } from '../../../utils';
|
||||
@@ -8,7 +9,7 @@ import { ios as iosBackground, Background } from '../../styling/background';
|
||||
import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty } from '../../styling/style-properties';
|
||||
import { profile } from '../../../profiling';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
|
||||
import { IOSPostAccessibilityNotificationType, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
|
||||
import { CoreTypes } from '../../../core-types';
|
||||
import type { ModalTransition } from '../../transition/modal-transition';
|
||||
import { SharedTransition } from '../../transition/shared-transition';
|
||||
@@ -23,7 +24,7 @@ const PFLAG_FORCE_LAYOUT = 1;
|
||||
const PFLAG_MEASURED_DIMENSION_SET = 1 << 1;
|
||||
const PFLAG_LAYOUT_REQUIRED = 1 << 2;
|
||||
|
||||
export class View extends ViewCommon implements ViewDefinition {
|
||||
export class View extends ViewCommon {
|
||||
// @ts-ignore
|
||||
nativeViewProtected: UIView;
|
||||
// @ts-ignore
|
||||
@@ -371,7 +372,7 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
};
|
||||
}
|
||||
|
||||
public getLocationRelativeTo(otherView: ViewDefinition): Point {
|
||||
public getLocationRelativeTo(otherView: View): Point {
|
||||
if (!this.nativeViewProtected || !this.nativeViewProtected.window || !otherView.nativeViewProtected || !otherView.nativeViewProtected.window || this.nativeViewProtected.window !== otherView.nativeViewProtected.window) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Definitions.
|
||||
import { View as ViewDefinition, Point, Size, ShownModallyData, Position } from '.';
|
||||
|
||||
import type { View as ViewType } from '.';
|
||||
import { Point, Size, ShownModallyData, Position } from './view-interfaces';
|
||||
import { booleanConverter, ShowModalOptions, ViewBase } from '../view-base';
|
||||
import { getEventOrGestureName } from '../bindable';
|
||||
import { layout } from '../../../utils';
|
||||
@@ -12,7 +11,7 @@ import { EventData } from '../../../data/observable';
|
||||
import { Trace } from '../../../trace';
|
||||
import { CoreTypes } from '../../../core-types';
|
||||
import { ViewHelper } from './view-helper';
|
||||
import { setupAccessibleView } from '../../../accessibility';
|
||||
import { setupAccessibleView } from '../../../application';
|
||||
|
||||
import { PercentLength } from '../../styling/style-properties';
|
||||
|
||||
@@ -26,9 +25,9 @@ import { LinearGradient } from '../../styling/linear-gradient';
|
||||
|
||||
import { Animation } from '../../animation';
|
||||
import type { AnimationPromise } from '../../animation/animation-types';
|
||||
import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
|
||||
import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, getFontScale } from '../../../accessibility';
|
||||
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent, getCurrentFontScale } from '../../../accessibility';
|
||||
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent } from '../../../accessibility';
|
||||
import { ShadowCSSValues } from '../../styling/css-shadow';
|
||||
import { SharedTransition, SharedTransitionInteractiveOptions } from '../../transition/shared-transition';
|
||||
import { Flex, FlexFlow } from '../../layouts/flexbox-layout';
|
||||
@@ -42,7 +41,7 @@ export function CSSType(type: string): ClassDecorator {
|
||||
};
|
||||
}
|
||||
|
||||
export function viewMatchesModuleContext(view: ViewDefinition, context: ModuleContext, types: ModuleType[]): boolean {
|
||||
export function viewMatchesModuleContext(view: ViewCommon, context: ModuleContext, types: ModuleType[]): boolean {
|
||||
return context && view._moduleName && context.type && types.some((type) => type === context.type) && context.path && context.path.includes(view._moduleName);
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ type InteractiveTransitionState = { began?: boolean; cancelled?: boolean; option
|
||||
// TODO: remove once we fully switch to the new event system
|
||||
const warnedEvent = new Set<string>();
|
||||
|
||||
export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
export abstract class ViewCommon extends ViewBase {
|
||||
public static layoutChangedEvent = 'layoutChanged';
|
||||
public static shownModallyEvent = 'shownModally';
|
||||
public static showingModallyEvent = 'showingModally';
|
||||
@@ -102,7 +101,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
protected _closeModalCallback: Function;
|
||||
public _manager: any;
|
||||
public _modalParent: ViewCommon;
|
||||
public _modalParent?: ViewCommon;
|
||||
private _modalContext: any;
|
||||
private _modal: ViewCommon;
|
||||
|
||||
@@ -399,7 +398,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
public showModal(...args): ViewDefinition {
|
||||
public showModal(...args): ViewType {
|
||||
const { view, options } = this.getModalOptions(args);
|
||||
if (options.transition?.instance) {
|
||||
SharedTransition.updateState(options.transition?.instance.id, {
|
||||
@@ -436,7 +435,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
modalRootViewCssClasses.forEach((c) => this.cssClasses.add(c));
|
||||
|
||||
parent._modal = this;
|
||||
this.style.fontScaleInternal = getCurrentFontScale();
|
||||
this.style.fontScaleInternal = getFontScale();
|
||||
this._modalParent = parent;
|
||||
this._modalContext = options.context;
|
||||
this._closeModalCallback = (...originalArgs) => {
|
||||
@@ -1050,7 +1049,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
return ViewHelper.combineMeasuredStates(curState, newState);
|
||||
}
|
||||
|
||||
public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number, setFrame = true): void {
|
||||
public static layoutChild(parent: ViewCommon, child: ViewCommon, left: number, top: number, right: number, bottom: number, setFrame = true): void {
|
||||
ViewHelper.layoutChild(parent, child, left, top, right, bottom);
|
||||
}
|
||||
|
||||
@@ -1089,7 +1088,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
this.eachChildView(<any>callback);
|
||||
}
|
||||
|
||||
public eachChildView(callback: (view: ViewDefinition) => boolean) {
|
||||
public eachChildView(callback: (view: ViewCommon) => boolean) {
|
||||
//
|
||||
}
|
||||
|
||||
@@ -1117,7 +1116,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getLocationRelativeTo(otherView: ViewDefinition): Point {
|
||||
public getLocationRelativeTo(otherView: ViewCommon): Point {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1224,11 +1223,11 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
//
|
||||
}
|
||||
|
||||
_hasAncestorView(ancestorView: ViewDefinition): boolean {
|
||||
const matcher = (view: ViewDefinition) => view === ancestorView;
|
||||
_hasAncestorView(ancestorView: ViewCommon): boolean {
|
||||
const matcher = (view: ViewCommon) => view === ancestorView;
|
||||
|
||||
for (let parent = this.parent; parent != null; parent = parent.parent) {
|
||||
if (matcher(<ViewDefinition>parent)) {
|
||||
if (matcher(<ViewCommon>parent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Types
|
||||
import { Position, View } from '..';
|
||||
import { Position } from '../view-interfaces';
|
||||
import type { View } from '..';
|
||||
|
||||
// Requires
|
||||
import { ViewHelper } from './view-helper-common';
|
||||
|
||||
61
packages/core/ui/core/view/view-interfaces.ts
Normal file
61
packages/core/ui/core/view/view-interfaces.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { EventData } from '../../../data/observable';
|
||||
/**
|
||||
* Defines the data for the shownModally event.
|
||||
*/
|
||||
export interface ShownModallyData extends EventData {
|
||||
/**
|
||||
* The context (optional, may be undefined) passed to the view when shown modally.
|
||||
*/
|
||||
context?: any;
|
||||
|
||||
/**
|
||||
* A callback to call when you want to close the modally shown view.
|
||||
* Pass in any kind of arguments and you will receive when the callback parameter
|
||||
* of View.showModal is executed.
|
||||
*/
|
||||
closeCallback?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Point interface describes a two dimensional location.
|
||||
* It has two properties x and y, representing the x and y coordinate of the location.
|
||||
*/
|
||||
export interface Point {
|
||||
/**
|
||||
* Represents the x coordinate of the location.
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* Represents the y coordinate of the location.
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* Represents the z coordinate of the location.
|
||||
*/
|
||||
z?: number;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Size interface describes abstract dimensions in two dimensional space.
|
||||
* It has two properties width and height, representing the width and height values of the size.
|
||||
*/
|
||||
export interface Size {
|
||||
/**
|
||||
* Represents the width of the size.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* Represents the height of the size.
|
||||
*/
|
||||
height: number;
|
||||
}
|
||||
@@ -685,7 +685,7 @@ export class FrameBase extends CustomLayoutView {
|
||||
}
|
||||
|
||||
// Handle markup/script changes in currentPage
|
||||
if (this.currentPage && viewMatchesModuleContext(this.currentPage, context, ['markup', 'script'])) {
|
||||
if (this.currentPage && viewMatchesModuleContext(this.currentPage as any, context, ['markup', 'script'])) {
|
||||
Trace.write(`Change Handled: Replacing page ${context.path}`, Trace.categories.Livesync);
|
||||
|
||||
// Replace current page with a default fade transition
|
||||
|
||||
@@ -15,7 +15,8 @@ export { ControlStateChangeListener } from './core/control-state-change';
|
||||
export { ViewBase, eachDescendant, getAncestor, getViewById, booleanConverter, querySelectorAll } from './core/view-base';
|
||||
export type { ShowModalOptions } from './core/view-base';
|
||||
export { View, CSSType, ContainerView, ViewHelper, AndroidHelper, IOSHelper, isUserInteractionEnabledProperty, PseudoClassHandler, CustomLayoutView } from './core/view';
|
||||
export type { Template, KeyedTemplate, ShownModallyData, AddArrayFromBuilder, AddChildFromBuilder, Size } from './core/view';
|
||||
export type { Template, KeyedTemplate, AddArrayFromBuilder, AddChildFromBuilder } from './core/view';
|
||||
export type { ShownModallyData, Size } from './core/view/view-interfaces';
|
||||
export { Property, CoercibleProperty, InheritedProperty, CssProperty, InheritedCssProperty, ShorthandProperty, CssAnimationProperty, unsetValue, makeParser, makeValidator } from './core/properties';
|
||||
export { addWeakEventListener, removeWeakEventListener } from './core/weak-event-listener';
|
||||
export { DatePicker } from './date-picker';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, FlexboxLayoutBase, FlexBasisPercent, orderProperty, flexGrowProperty, flexShrinkProperty, flexWrapBeforeProperty, alignSelfProperty } from './flexbox-layout-common';
|
||||
import { Position, View } from '../../core/view';
|
||||
import { View } from '../../core/view';
|
||||
import { Position } from '../../core/view/view-interfaces';
|
||||
import { layout } from '../../../utils';
|
||||
|
||||
export * from './flexbox-layout-common';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { StackLayoutBase } from './stack-layout-common';
|
||||
import { CoreTypes } from '../../../core-types';
|
||||
import { Position, View } from '../../core/view';
|
||||
import { View } from '../../core/view';
|
||||
import { Position } from '../../core/view/view-interfaces';
|
||||
import { layout } from '../../../utils';
|
||||
import { Trace } from '../../../trace';
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isAccessibilityServiceEnabled } from '../../application';
|
||||
import { PageBase, actionBarHiddenProperty, statusBarStyleProperty, androidStatusBarBackgroundProperty } from './page-common';
|
||||
import { CoreTypes } from '../../core-types';
|
||||
import { View } from '../core/view';
|
||||
import { Color } from '../../color';
|
||||
import { ActionBar } from '../action-bar';
|
||||
import { GridLayout } from '../layouts/grid-layout';
|
||||
import { SDK_VERSION } from '../../utils/constants';
|
||||
import { profile } from '../../profiling';
|
||||
import { AndroidAccessibilityEvent, getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
|
||||
import { AndroidAccessibilityEvent, getLastFocusedViewOnPage } from '../../accessibility';
|
||||
|
||||
export * from './page-common';
|
||||
|
||||
@@ -23,7 +23,7 @@ export class Page extends PageBase {
|
||||
JSON.stringify([
|
||||
{ value: 1, type: 0 /* org.nativescript.widgets.GridUnitType.auto */ },
|
||||
{ value: 1, type: 2 /* org.nativescript.widgets.GridUnitType.star */ },
|
||||
])
|
||||
]),
|
||||
);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Definitions.
|
||||
import { isAccessibilityServiceEnabled } from '../../application';
|
||||
import { Frame, BackstackEntry, NavigationType } from '../frame';
|
||||
|
||||
// Types.
|
||||
import { View, IOSHelper } from '../core/view';
|
||||
import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './page-common';
|
||||
|
||||
import { profile } from '../../profiling';
|
||||
import { layout } from '../../utils';
|
||||
import { SDK_VERSION } from '../../utils/constants';
|
||||
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
|
||||
import { getLastFocusedViewOnPage } from '../../accessibility';
|
||||
import { SharedTransition } from '../transition/shared-transition';
|
||||
|
||||
export * from './page-common';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Page as PageDefinition } from '.';
|
||||
import { ContentView } from '../content-view';
|
||||
import { View, CSSType, ShownModallyData } from '../core/view';
|
||||
import { View, CSSType } from '../core/view';
|
||||
import { ShownModallyData } from '../core/view/view-interfaces';
|
||||
import { booleanConverter } from '../core/view-base';
|
||||
import { Property, CssProperty } from '../core/properties';
|
||||
import { Style } from '../styling/style';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ScrollEventData } from '../scroll-view';
|
||||
import { Background as BackgroundDefinition } from './background';
|
||||
import { View, Point, Position } from '../core/view';
|
||||
import { View } from '../core/view';
|
||||
import { Point, Position } from '../core/view/view-interfaces';
|
||||
import { LinearGradient } from './linear-gradient';
|
||||
import { Screen } from '../../platform';
|
||||
import { isDataURI, isFileOrResourcePath, layout } from '../../utils';
|
||||
@@ -9,7 +10,6 @@ import { ImageSource } from '../../image-source';
|
||||
import type { CSSValue } from '../../css-value/reworkcss-value';
|
||||
import { parse as cssParse } from '../../css-value/reworkcss-value.js';
|
||||
import { BoxShadow } from './box-shadow';
|
||||
import { Length } from './style-properties';
|
||||
import { BackgroundClearFlags } from './background-common';
|
||||
import { ClipPathFunction } from './clip-path-function';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Observable } from '../../../data/observable';
|
||||
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf, FlexFlow, Flex } from '../../layouts/flexbox-layout';
|
||||
import { Trace } from '../../../trace';
|
||||
import { CoreTypes } from '../../../core-types';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility';
|
||||
import { ShadowCSSValues } from '../css-shadow';
|
||||
import { StrokeCSSValues } from '../css-stroke';
|
||||
import { ClipPathFunction } from '../clip-path-function';
|
||||
|
||||
@@ -19,6 +19,7 @@ global.NSObject = class NSObject {
|
||||
return new NSObject();
|
||||
}
|
||||
};
|
||||
global.NSNumber = {};
|
||||
global.NSString = {
|
||||
stringWithString() {
|
||||
return {
|
||||
@@ -106,6 +107,15 @@ global.NativeScriptUtils = {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
global.NSURLSessionConfiguration = {
|
||||
defaultSessionConfiguration: function () {
|
||||
return {};
|
||||
},
|
||||
ephemeralSessionConfiguration: function () {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
global.NSURLSessionTaskDelegate = function () {};
|
||||
global.NSOperationQueue = {
|
||||
mainQueue: {
|
||||
addOperationWithBlock(fn: Function) {
|
||||
@@ -148,6 +158,15 @@ global.UIResponder = function () {};
|
||||
global.UIResponder.extend = function () {};
|
||||
global.UIViewController = function () {};
|
||||
global.UIViewControllerTransitioningDelegate = function () {};
|
||||
global.UINavigationControllerDelegate = function () {};
|
||||
global.UINavigationController = function () {};
|
||||
global.UIUserInterfaceIdiom = {
|
||||
Phone: 0,
|
||||
Pad: 1,
|
||||
TV: 2,
|
||||
CarPlay: 3,
|
||||
Mac: 4,
|
||||
};
|
||||
global.UIGestureRecognizer = function () {};
|
||||
global.UIGestureRecognizerDelegate = function () {};
|
||||
global.UIAdaptivePresentationControllerDelegate = function () {};
|
||||
@@ -164,6 +183,7 @@ global.UIContentSizeCategoryAccessibilityLarge = 2.5;
|
||||
global.UIContentSizeCategoryAccessibilityExtraLarge = 3;
|
||||
global.UIContentSizeCategoryAccessibilityExtraExtraLarge = 3.5;
|
||||
global.UIContentSizeCategoryAccessibilityExtraExtraExtraLarge = 4;
|
||||
global.UIViewControllerAnimatedTransitioning = function () {};
|
||||
// global.UIDocumentInteractionController = {
|
||||
// interactionControllerWithURL(url: any) {
|
||||
// return null;
|
||||
|
||||
Reference in New Issue
Block a user