From 779def7609604ab2fc36509c6f277e603e95f8ec Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Tue, 8 Jul 2025 22:18:43 -0700 Subject: [PATCH] refactor: circular deps part 7 --- .../accessibility/accessibility-common.ts | 443 +++++++- .../accessibility/accessibility-css-helper.ts | 4 - .../accessibility/accessibility-css-init.ts | 121 --- .../accessibility/accessibility-properties.ts | 2 +- .../accessibility-service-common.ts | 35 - .../accessibility-service.android.ts | 132 --- .../accessibility/accessibility-service.d.ts | 10 - .../accessibility-service.ios.ts | 75 -- .../core/accessibility/accessibility-types.ts | 275 ----- .../core/accessibility/font-scale-common.ts | 15 - .../core/accessibility/font-scale.android.ts | 67 -- packages/core/accessibility/font-scale.d.ts | 9 - packages/core/accessibility/font-scale.ios.ts | 110 -- packages/core/accessibility/index.android.ts | 690 ------------ packages/core/accessibility/index.d.ts | 37 - packages/core/accessibility/index.ios.ts | 276 ----- packages/core/accessibility/index.ts | 1 + .../core/application/application-common.ts | 13 +- .../core/application/application.android.ts | 997 +++++++++++++++++- packages/core/application/application.d.ts | 35 + packages/core/application/application.ios.ts | 556 +++++++++- packages/core/fetch/index.js | 641 ----------- packages/core/fetch/index.ts | 620 +++++++++++ packages/core/project.json | 2 +- packages/core/ui/action-bar/index.android.ts | 2 +- packages/core/ui/core/view-base/index.ts | 37 +- packages/core/ui/core/view/index.android.ts | 9 +- packages/core/ui/core/view/index.d.ts | 62 +- packages/core/ui/core/view/index.ios.ts | 9 +- packages/core/ui/core/view/view-common.ts | 33 +- .../ui/core/view/view-helper/index.ios.ts | 3 +- packages/core/ui/core/view/view-interfaces.ts | 61 ++ packages/core/ui/frame/frame-common.ts | 2 +- packages/core/ui/index.ts | 3 +- .../ui/layouts/flexbox-layout/index.ios.ts | 3 +- .../core/ui/layouts/stack-layout/index.ios.ts | 3 +- packages/core/ui/page/index.android.ts | 6 +- packages/core/ui/page/index.ios.ts | 6 +- packages/core/ui/page/page-common.ts | 3 +- packages/core/ui/styling/background.ios.ts | 4 +- packages/core/ui/styling/style/index.ts | 2 +- packages/core/vitest.setup.ts | 20 + 42 files changed, 2798 insertions(+), 2636 deletions(-) delete mode 100644 packages/core/accessibility/accessibility-css-helper.ts delete mode 100644 packages/core/accessibility/accessibility-css-init.ts delete mode 100644 packages/core/accessibility/accessibility-service-common.ts delete mode 100644 packages/core/accessibility/accessibility-service.android.ts delete mode 100644 packages/core/accessibility/accessibility-service.d.ts delete mode 100644 packages/core/accessibility/accessibility-service.ios.ts delete mode 100644 packages/core/accessibility/accessibility-types.ts delete mode 100644 packages/core/accessibility/font-scale-common.ts delete mode 100644 packages/core/accessibility/font-scale.android.ts delete mode 100644 packages/core/accessibility/font-scale.d.ts delete mode 100644 packages/core/accessibility/font-scale.ios.ts delete mode 100644 packages/core/accessibility/index.android.ts delete mode 100644 packages/core/accessibility/index.d.ts delete mode 100644 packages/core/accessibility/index.ios.ts create mode 100644 packages/core/accessibility/index.ts delete mode 100644 packages/core/fetch/index.js create mode 100644 packages/core/fetch/index.ts create mode 100644 packages/core/ui/core/view/view-interfaces.ts diff --git a/packages/core/accessibility/accessibility-common.ts b/packages/core/accessibility/accessibility-common.ts index 7ca9cdfd9..5eaea0569 100644 --- a/packages/core/accessibility/accessibility-common.ts +++ b/packages/core/accessibility/accessibility-common.ts @@ -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; +export function setFontScaleCssClasses(value: Map) { + 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; +} diff --git a/packages/core/accessibility/accessibility-css-helper.ts b/packages/core/accessibility/accessibility-css-helper.ts deleted file mode 100644 index 2244b86af..000000000 --- a/packages/core/accessibility/accessibility-css-helper.ts +++ /dev/null @@ -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'; diff --git a/packages/core/accessibility/accessibility-css-init.ts b/packages/core/accessibility/accessibility-css-init.ts deleted file mode 100644 index 1d3851969..000000000 --- a/packages/core/accessibility/accessibility-css-init.ts +++ /dev/null @@ -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; - -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 = >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 = >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); -} diff --git a/packages/core/accessibility/accessibility-properties.ts b/packages/core/accessibility/accessibility-properties.ts index ade765a5d..0ff7bbece 100644 --- a/packages/core/accessibility/accessibility-properties.ts +++ b/packages/core/accessibility/accessibility-properties.ts @@ -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(enumValues) { return (value: string): T | null => { diff --git a/packages/core/accessibility/accessibility-service-common.ts b/packages/core/accessibility/accessibility-service-common.ts deleted file mode 100644 index 8ea7503be..000000000 --- a/packages/core/accessibility/accessibility-service-common.ts +++ /dev/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); - } -} diff --git a/packages/core/accessibility/accessibility-service.android.ts b/packages/core/accessibility/accessibility-service.android.ts deleted file mode 100644 index 757963c35..000000000 --- a/packages/core/accessibility/accessibility-service.android.ts +++ /dev/null @@ -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()); - } -} diff --git a/packages/core/accessibility/accessibility-service.d.ts b/packages/core/accessibility/accessibility-service.d.ts deleted file mode 100644 index 4a7fbd2db..000000000 --- a/packages/core/accessibility/accessibility-service.d.ts +++ /dev/null @@ -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; diff --git a/packages/core/accessibility/accessibility-service.ios.ts b/packages/core/accessibility/accessibility-service.ios.ts deleted file mode 100644 index 116986a49..000000000 --- a/packages/core/accessibility/accessibility-service.ios.ts +++ /dev/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()); - } -} diff --git a/packages/core/accessibility/accessibility-types.ts b/packages/core/accessibility/accessibility-types.ts deleted file mode 100644 index 48e3cac44..000000000 --- a/packages/core/accessibility/accessibility-types.ts +++ /dev/null @@ -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; -} diff --git a/packages/core/accessibility/font-scale-common.ts b/packages/core/accessibility/font-scale-common.ts deleted file mode 100644 index 976793f43..000000000 --- a/packages/core/accessibility/font-scale-common.ts +++ /dev/null @@ -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', -} diff --git a/packages/core/accessibility/font-scale.android.ts b/packages/core/accessibility/font-scale.android.ts deleted file mode 100644 index 46469b305..000000000 --- a/packages/core/accessibility/font-scale.android.ts +++ /dev/null @@ -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(); -} diff --git a/packages/core/accessibility/font-scale.d.ts b/packages/core/accessibility/font-scale.d.ts deleted file mode 100644 index bfc8635f2..000000000 --- a/packages/core/accessibility/font-scale.d.ts +++ /dev/null @@ -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; diff --git a/packages/core/accessibility/font-scale.ios.ts b/packages/core/accessibility/font-scale.ios.ts deleted file mode 100644 index 054366b61..000000000 --- a/packages/core/accessibility/font-scale.ios.ts +++ /dev/null @@ -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([ - [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(); -} diff --git a/packages/core/accessibility/index.android.ts b/packages/core/accessibility/index.android.ts deleted file mode 100644 index db36c9816..000000000 --- a/packages/core/accessibility/index.android.ts +++ /dev/null @@ -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(); - -let lastFocusedView: WeakRef; -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>(); - -let accessibilityEventMap: Map; -let accessibilityEventTypeMap: Map; - -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.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([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([ - /** - * 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(); -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(); - - // 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; -} diff --git a/packages/core/accessibility/index.d.ts b/packages/core/accessibility/index.d.ts deleted file mode 100644 index cb7a9d825..000000000 --- a/packages/core/accessibility/index.d.ts +++ /dev/null @@ -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; diff --git a/packages/core/accessibility/index.ios.ts b/packages/core/accessibility/index.ios.ts deleted file mode 100644 index 8e8c8c3d6..000000000 --- a/packages/core/accessibility/index.ios.ts +++ /dev/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): 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; -let RoleTypeMap: Map; - -let nativeFocusedNotificationObserver; -let lastFocusedView: WeakRef; -function ensureNativeClasses() { - if (AccessibilityTraitsMap && nativeFocusedNotificationObserver) { - return; - } - - AccessibilityTraitsMap = new Map([ - [AccessibilityTrait.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction], - [AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn], - [AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled], - [AccessibilityTrait.Selected, UIAccessibilityTraitSelected], - [AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently], - ]); - - RoleTypeMap = new Map([ - [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(uiView?.tag); - if (!view) { - for (const modalView of >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; -} diff --git a/packages/core/accessibility/index.ts b/packages/core/accessibility/index.ts new file mode 100644 index 000000000..a6f49f8a7 --- /dev/null +++ b/packages/core/accessibility/index.ts @@ -0,0 +1 @@ +export * from './accessibility-common'; diff --git a/packages/core/application/application-common.ts b/packages/core/application/application-common.ts index 33c58defd..6537e4279 100644 --- a/packages/core/application/application-common.ts +++ b/packages/core/application/application-common.ts @@ -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({ 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; } diff --git a/packages/core/application/application.android.ts b/packages/core/application/application.android.ts index 58e9cc0d6..4b160d709 100644 --- a/packages/core/application/application.android.ts +++ b/packages/core/application/application.android.ts @@ -1,11 +1,48 @@ import { profile } from '../profiling'; -import { View } from '../ui/core/view'; +import type { View } from '../ui/core/view'; import { isEmbedded } from '../ui/embedding'; +import { GestureTypes } from '../ui/gestures'; import { AndroidActivityCallbacks, NavigationEntry } from '../ui/frame/frame-common'; import { SDK_VERSION } from '../utils/constants'; -import type { AndroidApplication as IAndroidApplication } from './application'; +import { android as androidUtils } from '../utils'; import { ApplicationCommon } from './application-common'; import type { AndroidActivityBundleEventData, AndroidActivityEventData, ApplicationEventData } from './application-interfaces'; +import { Observable } from '../data/observable'; +import { Trace } from '../trace'; +import * as Utils from '../utils'; +import { + CommonA11YServiceEnabledObservable, + SharedA11YObservable, + notifyAccessibilityFocusState, + a11yServiceClasses, + a11yServiceDisabledClass, + a11yServiceEnabledClass, + fontScaleCategoryClasses, + fontScaleExtraLargeCategoryClass, + fontScaleExtraSmallCategoryClass, + fontScaleMediumCategoryClass, + getCurrentA11YServiceClass, + getCurrentFontScaleCategory, + getCurrentFontScaleClass, + getFontScaleCssClasses, + setCurrentA11YServiceClass, + setCurrentFontScaleCategory, + setCurrentFontScaleClass, + setFontScaleCssClasses, + setFontScale, + getFontScale, + setInitFontScale, + getFontScaleCategory, + setInitAccessibilityCssHelper, + FontScaleCategory, + getClosestValidFontScale, + VALID_FONT_SCALES, + AccessibilityRole, + AccessibilityState, + AndroidAccessibilityEvent, + isA11yEnabled, + setA11yEnabled, +} from '../accessibility/accessibility-common'; declare namespace com { namespace tns { @@ -285,7 +322,7 @@ function initNativeScriptComponentCallbacks() { return NativeScriptComponentCallbacks_; } -export class AndroidApplication extends ApplicationCommon implements IAndroidApplication { +export class AndroidApplication extends ApplicationCommon { static readonly activityCreatedEvent = 'activityCreated'; static readonly activityDestroyedEvent = 'activityDestroyed'; static readonly activityStartedEvent = 'activityStarted'; @@ -550,3 +587,957 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp export * from './application-common'; export const Application = new AndroidApplication(); export const iOSApplication = 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, + } as ApplicationEventData); + } +} + +export function getCurrentFontScale(): number { + setupConfigListener(); + + return getFontScale(); +} + +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); +} + +setInitFontScale(setupConfigListener); + +function applyRootCssClass(cssClasses: string[], newCssClass: string): void { + const rootView = Application.getRootView(); + if (!rootView) { + return; + } + + Application.applyCssClass(rootView, cssClasses, newCssClass); + + const rootModalViews = >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 = >rootView._getRootModalViews(); + rootModalViews.forEach((rootModalView) => (rootModalView.style.fontScaleInternal = fontScale)); +} + +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 class AccessibilityServiceEnabledObservable extends CommonA11YServiceEnabledObservable { + constructor() { + super(ensureStateListener()); + } +} + +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()); + } +} + +export function initAccessibilityCssHelper(): void { + ensureClasses(); + + Application.on(Application.fontScaleChangedEvent, () => { + updateCurrentHelperClasses(applyRootCssClass); + applyFontScaleToRootViews(); + }); + + accessibilityServiceObservable.on(AccessibilityServiceEnabledObservable.propertyChangeEvent, () => updateCurrentHelperClasses(applyRootCssClass)); +} +setInitAccessibilityCssHelper(initAccessibilityCssHelper); + +let clickableRolesMap = new Set(); + +let lastFocusedView: WeakRef; +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>(); + +let accessibilityEventMap: Map; +let accessibilityEventTypeMap: Map; + +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.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([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([ + /** + * 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])); +} + +function updateAccessibilityServiceState() { + const accessibilityManager = getAndroidAccessibilityManager(); + if (!accessibilityManager) { + return; + } + + setA11yEnabled(!!accessibilityManager.isEnabled() && !!accessibilityManager.isTouchExplorationEnabled()); +} + +export function isAccessibilityServiceEnabled(): boolean { + const accessibilityServiceEnabled = isA11yEnabled(); + 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(); +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(); + + // 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; +} diff --git a/packages/core/application/application.d.ts b/packages/core/application/application.d.ts index c61ba0098..e8fd9dc84 100644 --- a/packages/core/application/application.d.ts +++ b/packages/core/application/application.d.ts @@ -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; diff --git a/packages/core/application/application.ios.ts b/packages/core/application/application.ios.ts index c476d8d16..1675dcd0d 100644 --- a/packages/core/application/application.ios.ts +++ b/packages/core/application/application.ios.ts @@ -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>(); 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([ + [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): 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; +let RoleTypeMap: Map; + +let nativeFocusedNotificationObserver; +let lastFocusedView: WeakRef; +function ensureNativeClasses() { + if (AccessibilityTraitsMap && nativeFocusedNotificationObserver) { + return; + } + + AccessibilityTraitsMap = new Map([ + [AccessibilityTrait.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction], + [AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn], + [AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled], + [AccessibilityTrait.Selected, UIAccessibilityTraitSelected], + [AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently], + ]); + + RoleTypeMap = new Map([ + [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(uiView?.tag); + if (!view) { + for (const modalView of >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 = >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 = >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); diff --git a/packages/core/fetch/index.js b/packages/core/fetch/index.js deleted file mode 100644 index 63b027e93..000000000 --- a/packages/core/fetch/index.js +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/packages/core/fetch/index.ts b/packages/core/fetch/index.ts new file mode 100644 index 000000000..25a7907b5 --- /dev/null +++ b/packages/core/fetch/index.ts @@ -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 { + 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; + +export class Headers { + private map: Record = {}; + + 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)[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 { + const items: string[] = []; + this.forEach((_, name) => items.push(name)); + return iteratorFor(items); + } + + values(): IterableIterator { + 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 { + const rejected = consumed(this); + if (rejected) { + return rejected as Promise; + } + + 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 { + if (this._bodyArrayBuffer) { + const consumedResult = consumed(this); + if (consumedResult) { + return consumedResult as Promise; + } 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 { + const rejected = consumed(this); + if (rejected) { + return rejected as Promise; + } + + 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 { + return this.text().then(decode); + } + + json(): Promise { + return this.text().then(JSON.parse); + } +} + +// Helper functions for Body +function consumed(body: any): Promise | undefined { + if (body._noBody) return; + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')); + } + body.bodyUsed = true; +} + +function fileReaderReady(reader: FileReader): Promise { + return new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + }); +} + +function readBlobAsArrayBuffer(blob: Blob): Promise { + const reader = new FileReader(); + const promise = fileReaderReady(reader); + reader.readAsArrayBuffer(blob); + return promise; +} + +function readBlobAsText(blob: Blob): Promise { + 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 { + 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; +} diff --git a/packages/core/project.json b/packages/core/project.json index 28853c1ad..7fb25d8bb 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -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/" }, diff --git a/packages/core/ui/action-bar/index.android.ts b/packages/core/ui/action-bar/index.android.ts index 7792e5c13..5e570ca5a 100644 --- a/packages/core/ui/action-bar/index.android.ts +++ b/packages/core/ui/action-bar/index.android.ts @@ -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'; diff --git a/packages/core/ui/core/view-base/index.ts b/packages/core/ui/core/view-base/index.ts index 26d251ba0..8beebac20 100644 --- a/packages/core/ui/core/view-base/index.ts +++ b/packages/core/ui/core/view-base/index.ts @@ -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 { +export function querySelectorAll(view: ViewBase, selector: string): Array { if (!view) { return []; } - const retVal: Array = []; + const retVal: Array = []; 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 = 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(id: string): T { + getViewById(id: string): T { return getViewById(this, id); } /** * Returns the child view with the specified domId. */ - getViewByDomId(domId: number): T { + getViewByDomId(domId: number): T { return getViewByDomId(this, domId); } diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts index efea8e349..28d0b7d6c 100644 --- a/packages/core/ui/core/view/index.android.ts +++ b/packages/core/ui/core/view/index.android.ts @@ -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() { diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 7186aaeb6..b79cd796b 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -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. diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index d7f4918af..190b88a49 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -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; } diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index 98285f566..a9c49a5b9 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -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(); -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(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(parent)) { + if (matcher(parent)) { return true; } } diff --git a/packages/core/ui/core/view/view-helper/index.ios.ts b/packages/core/ui/core/view/view-helper/index.ios.ts index 4c3c0f33c..cf9898b7d 100644 --- a/packages/core/ui/core/view/view-helper/index.ios.ts +++ b/packages/core/ui/core/view/view-helper/index.ios.ts @@ -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'; diff --git a/packages/core/ui/core/view/view-interfaces.ts b/packages/core/ui/core/view/view-interfaces.ts new file mode 100644 index 000000000..1e471f1a9 --- /dev/null +++ b/packages/core/ui/core/view/view-interfaces.ts @@ -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; +} diff --git a/packages/core/ui/frame/frame-common.ts b/packages/core/ui/frame/frame-common.ts index 3a42c2979..36e48f3a2 100644 --- a/packages/core/ui/frame/frame-common.ts +++ b/packages/core/ui/frame/frame-common.ts @@ -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 diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts index e76199e3b..91fe2fff1 100644 --- a/packages/core/ui/index.ts +++ b/packages/core/ui/index.ts @@ -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'; diff --git a/packages/core/ui/layouts/flexbox-layout/index.ios.ts b/packages/core/ui/layouts/flexbox-layout/index.ios.ts index 22b3913de..dcb3ccd7c 100644 --- a/packages/core/ui/layouts/flexbox-layout/index.ios.ts +++ b/packages/core/ui/layouts/flexbox-layout/index.ios.ts @@ -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'; diff --git a/packages/core/ui/layouts/stack-layout/index.ios.ts b/packages/core/ui/layouts/stack-layout/index.ios.ts index 64e347d69..7c3b21594 100644 --- a/packages/core/ui/layouts/stack-layout/index.ios.ts +++ b/packages/core/ui/layouts/stack-layout/index.ios.ts @@ -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'; diff --git a/packages/core/ui/page/index.android.ts b/packages/core/ui/page/index.android.ts index 2a75f27b0..15e19f1e6 100644 --- a/packages/core/ui/page/index.android.ts +++ b/packages/core/ui/page/index.android.ts @@ -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; } diff --git a/packages/core/ui/page/index.ios.ts b/packages/core/ui/page/index.ios.ts index a36bce5fb..7288d9d25 100644 --- a/packages/core/ui/page/index.ios.ts +++ b/packages/core/ui/page/index.ios.ts @@ -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'; diff --git a/packages/core/ui/page/page-common.ts b/packages/core/ui/page/page-common.ts index 7843b4332..5f0dd7d80 100644 --- a/packages/core/ui/page/page-common.ts +++ b/packages/core/ui/page/page-common.ts @@ -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'; diff --git a/packages/core/ui/styling/background.ios.ts b/packages/core/ui/styling/background.ios.ts index 01632991a..52d9baa10 100644 --- a/packages/core/ui/styling/background.ios.ts +++ b/packages/core/ui/styling/background.ios.ts @@ -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'; diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 8002a4286..a9cebac74 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -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'; diff --git a/packages/core/vitest.setup.ts b/packages/core/vitest.setup.ts index c08256d22..b9e95a540 100644 --- a/packages/core/vitest.setup.ts +++ b/packages/core/vitest.setup.ts @@ -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;