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

committed by
Nathan Walker

parent
ef9c3b1f5f
commit
c46da3fad9
@ -6,6 +6,7 @@ import { layout, RESOURCE_PREFIX, isFontIconURI } from '../../utils';
|
||||
import { colorProperty } from '../styling/style-properties';
|
||||
import { ImageSource } from '../../image-source';
|
||||
import * as application from '../../application';
|
||||
import { isAccessibilityServiceEnabled, updateContentDescription } from '../../accessibility';
|
||||
|
||||
export * from './action-bar-common';
|
||||
|
||||
@ -298,7 +299,7 @@ export class ActionBar extends ActionBarBase {
|
||||
}
|
||||
}
|
||||
|
||||
public _updateTitleAndTitleView() {
|
||||
public _updateTitleAndTitleView(): void {
|
||||
if (!this.titleView) {
|
||||
// No title view - show the title
|
||||
const title = this.title;
|
||||
@ -313,6 +314,9 @@ export class ActionBar extends ActionBarBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update content description for the screen reader.
|
||||
updateContentDescription(this, true);
|
||||
}
|
||||
|
||||
public _addActionItems() {
|
||||
@ -447,6 +451,74 @@ export class ActionBar extends ActionBarBase {
|
||||
this.nativeViewProtected.setContentInsetsAbsolute(this.effectiveContentInsetLeft, this.effectiveContentInsetRight);
|
||||
}
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nativeView = this.nativeViewProtected;
|
||||
if (!nativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFocusableState = android.os.Build.VERSION.SDK_INT >= 26 && nativeView.getFocusable();
|
||||
const originalImportantForAccessibility = nativeView.getImportantForAccessibility();
|
||||
const originalIsAccessibilityHeading = android.os.Build.VERSION.SDK_INT >= 28 && nativeView.isAccessibilityHeading();
|
||||
|
||||
try {
|
||||
nativeView.setFocusable(false);
|
||||
nativeView.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
|
||||
let announceView: android.view.View | null = null;
|
||||
|
||||
const numChildren = nativeView.getChildCount();
|
||||
for (let i = 0; i < numChildren; i += 1) {
|
||||
const childView = nativeView.getChildAt(i);
|
||||
if (!childView) {
|
||||
continue;
|
||||
}
|
||||
|
||||
childView.setFocusable(true);
|
||||
if (childView instanceof androidx.appcompat.widget.AppCompatTextView) {
|
||||
announceView = childView;
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
announceView.setAccessibilityHeading(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!announceView) {
|
||||
announceView = nativeView;
|
||||
}
|
||||
|
||||
announceView.setFocusable(true);
|
||||
announceView.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
// Reset status after the focus have been reset.
|
||||
const localNativeView = this.nativeViewProtected;
|
||||
if (!localNativeView) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
nativeView.setAccessibilityHeading(originalIsAccessibilityHeading);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 26) {
|
||||
localNativeView.setFocusable(originalFocusableState);
|
||||
}
|
||||
|
||||
localNativeView.setImportantForAccessibility(originalImportantForAccessibility);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAppCompatTextView(toolbar: androidx.appcompat.widget.Toolbar): typeof AppCompatTextView {
|
||||
|
@ -5,6 +5,7 @@ import { Color } from '../../color';
|
||||
import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties';
|
||||
import { ImageSource } from '../../image-source';
|
||||
import { layout, iOSNativeHelper, isFontIconURI } from '../../utils';
|
||||
import { accessibilityHintProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityValueProperty } from '../../accessibility/accessibility-properties';
|
||||
|
||||
export * from './action-bar-common';
|
||||
|
||||
@ -99,6 +100,46 @@ export class ActionBar extends ActionBarBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
[accessibilityValueProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityValue = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityLabelProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLabel = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityLabel = value;
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityHintProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityHint = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityHint = value;
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityLanguageProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLanguage = value;
|
||||
|
||||
const navigationItem = this._getNavigationItem();
|
||||
if (navigationItem) {
|
||||
navigationItem.accessibilityLanguage = value;
|
||||
}
|
||||
}
|
||||
|
||||
public createNativeView(): UIView {
|
||||
return this.ios;
|
||||
}
|
||||
@ -148,7 +189,18 @@ export class ActionBar extends ActionBarBase {
|
||||
}
|
||||
}
|
||||
|
||||
public update() {
|
||||
private _getNavigationItem(): UINavigationItem | null {
|
||||
const page = this.page;
|
||||
// Page should be attached to frame to update the action bar.
|
||||
if (!page || !page.frame) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewController = <UIViewController>page.ios;
|
||||
return viewController.navigationItem;
|
||||
}
|
||||
|
||||
public update(): void {
|
||||
const page = this.page;
|
||||
// Page should be attached to frame to update the action bar.
|
||||
if (!page || !page.frame) {
|
||||
@ -228,6 +280,12 @@ export class ActionBar extends ActionBarBase {
|
||||
if (!this.isLayoutValid) {
|
||||
this.layoutInternal();
|
||||
}
|
||||
|
||||
// Make sure accessibility values are up-to-date on the navigationItem
|
||||
navigationItem.accessibilityValue = this.accessibilityValue;
|
||||
navigationItem.accessibilityLabel = this.accessibilityLabel;
|
||||
navigationItem.accessibilityLanguage = this.accessibilityLanguage;
|
||||
navigationItem.accessibilityHint = this.accessibilityHint;
|
||||
}
|
||||
|
||||
private populateMenuItems(navigationItem: UINavigationItem) {
|
||||
|
@ -2,11 +2,15 @@ import { Button as ButtonDefinition } from '.';
|
||||
import { TextBase } from '../text-base';
|
||||
import { CSSType } from '../core/view';
|
||||
import { booleanConverter } from '../core/view-base';
|
||||
import { AccessibilityRole } from '../../accessibility';
|
||||
|
||||
@CSSType('Button')
|
||||
export abstract class ButtonBase extends TextBase implements ButtonDefinition {
|
||||
public static tapEvent = 'tap';
|
||||
|
||||
accessible = true;
|
||||
accessibilityRole = AccessibilityRole.Button;
|
||||
|
||||
get textWrap(): boolean {
|
||||
return this.style.whiteSpace === 'normal';
|
||||
}
|
||||
|
13
packages/core/ui/core/view-base/index.d.ts
vendored
13
packages/core/ui/core/view-base/index.d.ts
vendored
@ -34,6 +34,14 @@ export function isEventOrGesture(name: string, view: ViewBase): boolean;
|
||||
*/
|
||||
export function getViewById(view: ViewBase, id: string): ViewBase;
|
||||
|
||||
/**
|
||||
* Gets a child view by domId.
|
||||
* @param view - The parent (container) view of the view to look for.
|
||||
* @param domId - The id of the view to look for.
|
||||
* Returns an instance of a view (if found), otherwise undefined.
|
||||
*/
|
||||
export function getViewByDomId(view: ViewBase, domId: number): ViewBase;
|
||||
|
||||
export interface ShowModalOptions {
|
||||
/**
|
||||
* Any context you want to pass to the modally shown view. This same context will be available in the arguments of the shownModally event handler.
|
||||
@ -289,6 +297,11 @@ export abstract class ViewBase extends Observable {
|
||||
*/
|
||||
public getViewById<T extends ViewBase>(id: string): T;
|
||||
|
||||
/**
|
||||
* Returns the child view with the specified domId.
|
||||
*/
|
||||
public getViewByDomId<T extends ViewBase>(id: number): T;
|
||||
|
||||
/**
|
||||
* Load view.
|
||||
* @param view to load.
|
||||
|
@ -143,6 +143,32 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export function getViewByDomId(view: ViewBaseDefinition, domId: number): ViewBaseDefinition {
|
||||
if (!view) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (view._domId === domId) {
|
||||
return view;
|
||||
}
|
||||
|
||||
let retVal: ViewBaseDefinition;
|
||||
const descendantsCallback = function (child: ViewBaseDefinition): boolean {
|
||||
if (view._domId === domId) {
|
||||
retVal = child;
|
||||
|
||||
// break the iteration by returning false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
eachDescendant(view, descendantsCallback);
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewBaseDefinition) => boolean) {
|
||||
if (!callback || !view) {
|
||||
return;
|
||||
@ -373,6 +399,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
return <T>getViewById(this, id);
|
||||
}
|
||||
|
||||
getViewByDomId<T extends ViewBaseDefinition>(domId: number): T {
|
||||
return <T>getViewByDomId(this, domId);
|
||||
}
|
||||
|
||||
get page(): Page {
|
||||
if (this.parent) {
|
||||
return this.parent.page;
|
||||
|
@ -1,8 +1,9 @@
|
||||
// Definitions.
|
||||
import { Point, CustomLayoutView as CustomLayoutViewDefinition, dip } from '.';
|
||||
import { GestureTypes, GestureEventData } from '../../gestures';
|
||||
import type { Point, CustomLayoutView as CustomLayoutViewDefinition, dip } from '.';
|
||||
import type { GestureTypes, GestureEventData } from '../../gestures';
|
||||
|
||||
// Types.
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty } from '../../styling/style-properties';
|
||||
import { layout } from '../../../utils';
|
||||
import { Trace } from '../../../trace';
|
||||
@ -48,6 +49,9 @@ import { Screen } from '../../../platform';
|
||||
import { AndroidActivityBackPressedEventData, android as androidApp } from '../../../application';
|
||||
import { Device } from '../../../platform';
|
||||
import lazy from '../../../utils/lazy';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, setupAccessibleView, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription } from '../../../accessibility';
|
||||
import * as Utils from '../../../utils';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -323,6 +327,12 @@ export class View extends ViewCommon {
|
||||
|
||||
nativeViewProtected: android.view.View;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.on(View.loadedEvent, () => setupAccessibleView(this));
|
||||
}
|
||||
|
||||
// TODO: Implement unobserve that detach the touchListener.
|
||||
_observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void {
|
||||
super._observe(type, callback, thisArg);
|
||||
@ -744,13 +754,6 @@ export class View extends ViewCommon {
|
||||
org.nativescript.widgets.OriginPoint.setY(this.nativeViewProtected, value);
|
||||
}
|
||||
|
||||
[automationTextProperty.getDefault](): string {
|
||||
return this.nativeViewProtected.getContentDescription();
|
||||
}
|
||||
[automationTextProperty.setNative](value: string) {
|
||||
this.nativeViewProtected.setContentDescription(value);
|
||||
}
|
||||
|
||||
[isUserInteractionEnabledProperty.setNative](value: boolean) {
|
||||
this.nativeViewProtected.setClickable(value);
|
||||
this.nativeViewProtected.setFocusable(value);
|
||||
@ -792,6 +795,77 @@ export class View extends ViewCommon {
|
||||
this.nativeViewProtected.setAlpha(float(value));
|
||||
}
|
||||
|
||||
[accessibilityEnabledProperty.setNative](value: boolean): void {
|
||||
this.nativeViewProtected.setFocusable(!!value);
|
||||
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityIdentifierProperty.setNative](value: string): void {
|
||||
const id = Utils.ad.resources.getId(':id/nativescript_accessibility_id');
|
||||
|
||||
if (id) {
|
||||
this.nativeViewProtected.setTag(id, value);
|
||||
this.nativeViewProtected.setTag(value);
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityRoleProperty.setNative](value: AccessibilityRole): void {
|
||||
updateAccessibilityProperties(this);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= 28) {
|
||||
this.nativeViewProtected?.setAccessibilityHeading(value === AccessibilityRole.Header);
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityValueProperty.setNative](): void {
|
||||
this._androidContentDescriptionUpdated = true;
|
||||
updateContentDescription(this);
|
||||
}
|
||||
|
||||
[accessibilityLabelProperty.setNative](): void {
|
||||
this._androidContentDescriptionUpdated = true;
|
||||
updateContentDescription(this);
|
||||
}
|
||||
|
||||
[accessibilityHintProperty.setNative](): void {
|
||||
this._androidContentDescriptionUpdated = true;
|
||||
updateContentDescription(this);
|
||||
}
|
||||
|
||||
[accessibilityHiddenProperty.setNative](value: boolean): void {
|
||||
if (value) {
|
||||
this.nativeViewProtected.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
} else {
|
||||
this.nativeViewProtected.setImportantForAccessibility(android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityLiveRegionProperty.setNative](value: AccessibilityLiveRegion): void {
|
||||
switch (value) {
|
||||
case AccessibilityLiveRegion.Assertive: {
|
||||
this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE);
|
||||
break;
|
||||
}
|
||||
case AccessibilityLiveRegion.Polite: {
|
||||
this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.nativeViewProtected.setAccessibilityLiveRegion(android.view.View.ACCESSIBILITY_LIVE_REGION_NONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[accessibilityStateProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityMediaSessionProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[androidElevationProperty.getDefault](): number {
|
||||
return this.getDefaultElevation();
|
||||
}
|
||||
@ -1047,6 +1121,30 @@ export class View extends ViewCommon {
|
||||
(<any>nativeView).background = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAccessibilityEvent(this, eventName, msg);
|
||||
}
|
||||
|
||||
public accessibilityAnnouncement(msg = this.accessibilityLabel): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.androidSendAccessibilityEvent(AndroidAccessibilityEvent.ANNOUNCEMENT, msg);
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.androidSendAccessibilityEvent(AndroidAccessibilityEvent.WINDOW_STATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
export class ContainerView extends View {
|
||||
|
109
packages/core/ui/core/view/index.d.ts
vendored
109
packages/core/ui/core/view/index.d.ts
vendored
@ -4,8 +4,9 @@ import { EventData } from '../../../data/observable';
|
||||
import { Color } from '../../../color';
|
||||
import { Animation, AnimationDefinition, AnimationPromise } from '../../animation';
|
||||
import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from '../../styling/style-properties';
|
||||
import { GestureTypes, GestureEventData, GesturesObserver } from '../../gestures';
|
||||
import { GestureTypes, GesturesObserver } from '../../gestures';
|
||||
import { LinearGradient } from '../../styling/gradient';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AndroidAccessibilityEvent, IOSPostAccessibilityNotificationType } from '../../../accessibility/accessibility-types';
|
||||
import { BoxShadow } from '../../styling/box-shadow';
|
||||
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -131,6 +132,21 @@ export abstract class View extends ViewBase {
|
||||
*/
|
||||
public static shownModallyEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to accessibilityBlur event.
|
||||
*/
|
||||
public static accessibilityBlurEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to accessibilityFocus event.
|
||||
*/
|
||||
public static accessibilityFocusEvent: string;
|
||||
|
||||
/**
|
||||
* String value used when hooking to accessibilityFocusChanged event.
|
||||
*/
|
||||
public static accessibilityFocusChangedEvent: string;
|
||||
|
||||
/**
|
||||
* Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform.
|
||||
*/
|
||||
@ -226,6 +242,68 @@ export abstract class View extends ViewBase {
|
||||
*/
|
||||
color: Color;
|
||||
|
||||
/**
|
||||
* If `true` the element is an accessibility element and all the children will be treated as a single selectable component.
|
||||
*/
|
||||
accessible: boolean;
|
||||
|
||||
/**
|
||||
* Hide the view and its children from the a11y service
|
||||
*/
|
||||
accessibilityHidden: boolean;
|
||||
|
||||
/**
|
||||
* The view's unique accessibilityIdentifier.
|
||||
*
|
||||
* This is used for automated testing.
|
||||
*/
|
||||
accessibilityIdentifier: string;
|
||||
|
||||
/**
|
||||
* Which role should this view be treated by the a11y service?
|
||||
*/
|
||||
accessibilityRole: AccessibilityRole;
|
||||
|
||||
/**
|
||||
* Which state should this view be treated as by the a11y service?
|
||||
*/
|
||||
accessibilityState: AccessibilityState;
|
||||
|
||||
/**
|
||||
* Short description of the element, ideally one word.
|
||||
*/
|
||||
accessibilityLabel: string;
|
||||
|
||||
/**
|
||||
* Current value of the element in a localized string.
|
||||
*/
|
||||
accessibilityValue: string;
|
||||
|
||||
/**
|
||||
* A hint describes the elements behavior. Example: 'Tap change playback speed'
|
||||
*/
|
||||
accessibilityHint: string;
|
||||
accessibilityTraits?: AccessibilityTrait[];
|
||||
accessibilityLiveRegion: AccessibilityLiveRegion;
|
||||
|
||||
/**
|
||||
* Sets the language in which to speak the element's label and value.
|
||||
* Accepts language ID tags that follows the "BCP 47" specification.
|
||||
*/
|
||||
accessibilityLanguage: string;
|
||||
|
||||
/**
|
||||
* This view starts a media session. Equivalent to trait = startsMedia
|
||||
*/
|
||||
accessibilityMediaSession: boolean;
|
||||
|
||||
/**
|
||||
* Internal use only. This is used to limit the number of updates to android.view.View.setContentDescription()
|
||||
*/
|
||||
_androidContentDescriptionUpdated?: boolean;
|
||||
|
||||
automationText: string;
|
||||
|
||||
/**
|
||||
* Gets or sets the elevation of the android view.
|
||||
*/
|
||||
@ -364,11 +442,6 @@ export abstract class View extends ViewBase {
|
||||
|
||||
//END Style property shortcuts
|
||||
|
||||
/**
|
||||
* Gets or sets the automation text of the view.
|
||||
*/
|
||||
automationText: string;
|
||||
|
||||
/**
|
||||
* Gets or sets the X component of the origin point around which the view will be transformed. The default value is 0.5 representing the center of the view.
|
||||
*/
|
||||
@ -673,6 +746,29 @@ export abstract class View extends ViewBase {
|
||||
*/
|
||||
public eachChildView(callback: (view: View) => boolean): void;
|
||||
|
||||
/**
|
||||
* Android: Send accessibility event
|
||||
*/
|
||||
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void;
|
||||
|
||||
/**
|
||||
* iOS: post accessibility notification.
|
||||
* type = 'announcement' will announce `args` via VoiceOver. If no args element will be announced instead.
|
||||
* type = 'layout' used when the layout of a screen changes.
|
||||
* type = 'screen' large change made to the screen.
|
||||
*/
|
||||
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void;
|
||||
|
||||
/**
|
||||
* Make an announcement to the screen reader.
|
||||
*/
|
||||
public accessibilityAnnouncement(msg?: string): void;
|
||||
|
||||
/**
|
||||
* Announce screen changed
|
||||
*/
|
||||
public accessibilityScreenChanged(): void;
|
||||
|
||||
//@private
|
||||
/**
|
||||
* @private
|
||||
@ -879,7 +975,6 @@ export interface AddChildFromBuilder {
|
||||
_addChildFromBuilder(name: string, value: any): void;
|
||||
}
|
||||
|
||||
export const automationTextProperty: Property<View, string>;
|
||||
export const originXProperty: Property<View, number>;
|
||||
export const originYProperty: Property<View, number>;
|
||||
export const isEnabledProperty: Property<View, boolean>;
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Point, View as ViewDefinition, dip } from '.';
|
||||
|
||||
// Requires
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { ViewCommon, isEnabledProperty, originXProperty, originYProperty, isUserInteractionEnabledProperty } from './view-common';
|
||||
import { ShowModalOptions } from '../view-base';
|
||||
import { Trace } from '../../../trace';
|
||||
import { layout, iOSNativeHelper } from '../../../utils';
|
||||
@ -10,6 +10,8 @@ import { IOSHelper } from './view-helper';
|
||||
import { ios as iosBackground, Background } from '../../styling/background';
|
||||
import { perspectiveProperty, Visibility, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, clipPathProperty } from '../../styling/style-properties';
|
||||
import { profile } from '../../../profiling';
|
||||
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityTraitsProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { setupAccessibleView, IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties } from '../../../accessibility';
|
||||
|
||||
export * from './view-common';
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -54,6 +56,12 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
return (this._privateFlags & PFLAG_FORCE_LAYOUT) === PFLAG_FORCE_LAYOUT;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.once(View.loadedEvent, () => setupAccessibleView(this));
|
||||
}
|
||||
|
||||
public requestLayout(): void {
|
||||
super.requestLayout();
|
||||
this._privateFlags |= PFLAG_FORCE_LAYOUT;
|
||||
@ -553,14 +561,65 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
this.updateOriginPoint(this.originX, value);
|
||||
}
|
||||
|
||||
[automationTextProperty.getDefault](): string {
|
||||
[accessibilityEnabledProperty.setNative](value: boolean): void {
|
||||
this.nativeViewProtected.isAccessibilityElement = !!value;
|
||||
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityIdentifierProperty.getDefault](): string {
|
||||
return this.nativeViewProtected.accessibilityLabel;
|
||||
}
|
||||
[automationTextProperty.setNative](value: string) {
|
||||
[accessibilityIdentifierProperty.setNative](value: string): void {
|
||||
this.nativeViewProtected.accessibilityIdentifier = value;
|
||||
}
|
||||
|
||||
[accessibilityRoleProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityTraitsProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityValueProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityValue = value;
|
||||
}
|
||||
|
||||
[accessibilityLabelProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLabel = value;
|
||||
}
|
||||
|
||||
[accessibilityHintProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityHint = value;
|
||||
}
|
||||
|
||||
[accessibilityLanguageProperty.setNative](value: string): void {
|
||||
value = value == null ? null : `${value}`;
|
||||
this.nativeViewProtected.accessibilityLanguage = value;
|
||||
}
|
||||
|
||||
[accessibilityHiddenProperty.setNative](value: boolean): void {
|
||||
this.nativeViewProtected.accessibilityElementsHidden = !!value;
|
||||
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityLiveRegionProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityStateProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[accessibilityMediaSessionProperty.setNative](): void {
|
||||
updateAccessibilityProperties(this);
|
||||
}
|
||||
|
||||
[isUserInteractionEnabledProperty.getDefault](): boolean {
|
||||
return this.nativeViewProtected.userInteractionEnabled;
|
||||
}
|
||||
@ -673,6 +732,54 @@ export class View extends ViewCommon implements ViewDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void {
|
||||
if (!notificationType) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notification: number;
|
||||
let args: string | UIView | null = this.nativeViewProtected;
|
||||
if (typeof msg === 'string' && msg) {
|
||||
args = msg;
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case IOSPostAccessibilityNotificationType.Announcement: {
|
||||
notification = UIAccessibilityAnnouncementNotification;
|
||||
break;
|
||||
}
|
||||
case IOSPostAccessibilityNotificationType.Layout: {
|
||||
notification = UIAccessibilityLayoutChangedNotification;
|
||||
break;
|
||||
}
|
||||
case IOSPostAccessibilityNotificationType.Screen: {
|
||||
notification = UIAccessibilityScreenChangedNotification;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UIAccessibilityPostNotification(notification, args ?? null);
|
||||
}
|
||||
|
||||
public accessibilityAnnouncement(msg = this.accessibilityLabel): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iosPostAccessibilityNotification(IOSPostAccessibilityNotificationType.Announcement, msg);
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.iosPostAccessibilityNotification(IOSPostAccessibilityNotificationType.Screen);
|
||||
}
|
||||
|
||||
_getCurrentLayoutBounds(): {
|
||||
left: number;
|
||||
top: number;
|
||||
|
@ -22,6 +22,9 @@ import { LinearGradient } from '../../styling/linear-gradient';
|
||||
import { TextTransform } from '../../text-base';
|
||||
|
||||
import * as am from '../../animation';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AndroidAccessibilityEvent, IOSPostAccessibilityNotificationType } from '../../../accessibility/accessibility-types';
|
||||
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityTraitsProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
|
||||
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, getCurrentFontScale } from '../../../accessibility';
|
||||
import { BoxShadow } from '../../styling/box-shadow';
|
||||
|
||||
// helpers (these are okay re-exported here)
|
||||
@ -68,6 +71,9 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
public static layoutChangedEvent = 'layoutChanged';
|
||||
public static shownModallyEvent = 'shownModally';
|
||||
public static showingModallyEvent = 'showingModally';
|
||||
public static accessibilityBlurEvent = accessibilityBlurEvent;
|
||||
public static accessibilityFocusEvent = accessibilityFocusEvent;
|
||||
public static accessibilityFocusChangedEvent = accessibilityFocusChangedEvent;
|
||||
|
||||
protected _closeModalCallback: Function;
|
||||
public _manager: any;
|
||||
@ -91,6 +97,8 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
public _gestureObservers = {};
|
||||
|
||||
_androidContentDescriptionUpdated?: boolean;
|
||||
|
||||
get css(): string {
|
||||
const scope = this._styleScope;
|
||||
|
||||
@ -360,6 +368,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
modalRootViewCssClasses.forEach((c) => this.cssClasses.add(c));
|
||||
|
||||
parent._modal = this;
|
||||
this.style._fontScale = getCurrentFontScale();
|
||||
this._modalParent = parent;
|
||||
this._modalContext = options.context;
|
||||
const that = this;
|
||||
@ -743,6 +752,71 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
this.style.scaleY = value;
|
||||
}
|
||||
|
||||
get accessible(): boolean {
|
||||
return this.style.accessible;
|
||||
}
|
||||
set accessible(value: boolean) {
|
||||
this.style.accessible = value;
|
||||
}
|
||||
|
||||
get accessibilityHidden(): boolean {
|
||||
return this.style.accessibilityHidden;
|
||||
}
|
||||
set accessibilityHidden(value: boolean) {
|
||||
this.style.accessibilityHidden = value;
|
||||
}
|
||||
|
||||
public accessibilityIdentifier: string;
|
||||
|
||||
get accessibilityRole(): AccessibilityRole {
|
||||
return this.style.accessibilityRole;
|
||||
}
|
||||
set accessibilityRole(value: AccessibilityRole) {
|
||||
this.style.accessibilityRole = value;
|
||||
}
|
||||
|
||||
get accessibilityState(): AccessibilityState {
|
||||
return this.style.accessibilityState;
|
||||
}
|
||||
set accessibilityState(value: AccessibilityState) {
|
||||
this.style.accessibilityState = value;
|
||||
}
|
||||
|
||||
public accessibilityLabel: string;
|
||||
public accessibilityValue: string;
|
||||
public accessibilityHint: string;
|
||||
|
||||
get accessibilityLiveRegion(): AccessibilityLiveRegion {
|
||||
return this.style.accessibilityLiveRegion;
|
||||
}
|
||||
set accessibilityLiveRegion(value: AccessibilityLiveRegion) {
|
||||
this.style.accessibilityLiveRegion = value;
|
||||
}
|
||||
|
||||
get accessibilityLanguage(): string {
|
||||
return this.style.accessibilityLanguage;
|
||||
}
|
||||
set accessibilityLanguage(value: string) {
|
||||
this.style.accessibilityLanguage = value;
|
||||
}
|
||||
|
||||
get accessibilityMediaSession(): boolean {
|
||||
return this.style.accessibilityMediaSession;
|
||||
}
|
||||
set accessibilityMediaSession(value: boolean) {
|
||||
this.style.accessibilityMediaSession = value;
|
||||
}
|
||||
|
||||
public accessibilityTraits?: AccessibilityTrait[];
|
||||
|
||||
get automationText(): string {
|
||||
return this.accessibilityIdentifier;
|
||||
}
|
||||
|
||||
set automationText(value: string) {
|
||||
this.accessibilityIdentifier = value;
|
||||
}
|
||||
|
||||
get androidElevation(): number {
|
||||
return this.style.androidElevation;
|
||||
}
|
||||
@ -759,7 +833,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
//END Style property shortcuts
|
||||
|
||||
public automationText: string;
|
||||
public originX: number;
|
||||
public originY: number;
|
||||
public isEnabled: boolean;
|
||||
@ -1013,12 +1086,23 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const automationTextProperty = new Property<ViewCommon, string>({
|
||||
name: 'automationText',
|
||||
});
|
||||
automationTextProperty.register(ViewCommon);
|
||||
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public accessibilityAnnouncement(msg?: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const originXProperty = new Property<ViewCommon, number>({
|
||||
name: 'originX',
|
||||
@ -1070,3 +1154,9 @@ export const iosIgnoreSafeAreaProperty = new InheritedProperty({
|
||||
valueConverter: booleanConverter,
|
||||
});
|
||||
iosIgnoreSafeAreaProperty.register(ViewCommon);
|
||||
|
||||
accessibilityIdentifierProperty.register(ViewCommon);
|
||||
accessibilityLabelProperty.register(ViewCommon);
|
||||
accessibilityValueProperty.register(ViewCommon);
|
||||
accessibilityHintProperty.register(ViewCommon);
|
||||
accessibilityTraitsProperty.register(ViewCommon);
|
||||
|
@ -5,6 +5,7 @@ import { ActionBar } from '../action-bar';
|
||||
import { GridLayout } from '../layouts/grid-layout';
|
||||
import { Device } from '../../platform';
|
||||
import { profile } from '../../profiling';
|
||||
import { AndroidAccessibilityEvent, getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
|
||||
|
||||
export * from './page-common';
|
||||
|
||||
@ -122,4 +123,31 @@ export class Page extends PageBase {
|
||||
(<any>window).setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(refocus = false): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refocus) {
|
||||
const lastFocusedView = getLastFocusedViewOnPage(this);
|
||||
if (lastFocusedView) {
|
||||
const announceView = lastFocusedView.nativeViewProtected;
|
||||
if (announceView) {
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
||||
announceView.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionBarHidden || this.accessibilityLabel) {
|
||||
this.androidSendAccessibilityEvent(AndroidAccessibilityEvent.WINDOW_STATE_CHANGED);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionBar.accessibilityScreenChanged();
|
||||
}
|
||||
}
|
||||
|
10
packages/core/ui/page/index.d.ts
vendored
10
packages/core/ui/page/index.d.ts
vendored
@ -93,6 +93,11 @@ export declare class Page extends PageBase {
|
||||
*/
|
||||
public actionBar: ActionBar;
|
||||
|
||||
/**
|
||||
* Should page changed be annnounced to the screen reader.
|
||||
*/
|
||||
public accessibilityAnnouncePageEnabled = true;
|
||||
|
||||
/**
|
||||
* A basic method signature to hook an event listener (shortcut alias to the addEventListener method).
|
||||
* @param eventNames - String corresponding to events (e.g. "propertyChange"). Optionally could be used more events separated by `,` (e.g. "propertyChange", "change").
|
||||
@ -161,6 +166,11 @@ export declare class Page extends PageBase {
|
||||
*/
|
||||
public onNavigatedFrom(isBackNavigation: boolean): void;
|
||||
//@endprivate
|
||||
|
||||
/**
|
||||
* Announce screen changed
|
||||
*/
|
||||
public accessibilityScreenChanged(refocus?: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,7 @@ import { PageBase, actionBarHiddenProperty, statusBarStyleProperty } from './pag
|
||||
|
||||
import { profile } from '../../profiling';
|
||||
import { iOSNativeHelper, layout } from '../../utils';
|
||||
import { getLastFocusedViewOnPage, isAccessibilityServiceEnabled } from '../../accessibility';
|
||||
|
||||
export * from './page-common';
|
||||
|
||||
@ -522,6 +523,44 @@ export class Page extends PageBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(refocus = false): void {
|
||||
if (!isAccessibilityServiceEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refocus) {
|
||||
const lastFocusedView = getLastFocusedViewOnPage(this);
|
||||
if (lastFocusedView) {
|
||||
const uiView = lastFocusedView.nativeViewProtected;
|
||||
if (uiView) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, uiView);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionBarHidden) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.nativeViewProtected);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.accessibilityLabel) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.nativeViewProtected);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.actionBar.accessibilityLabel || this.actionBar.title) {
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.actionBar.nativeView);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, this.nativeViewProtected);
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateTopmostController(controller: UIViewController): void {
|
||||
|
@ -33,6 +33,7 @@ export class PageBase extends ContentView {
|
||||
public enableSwipeBackNavigation: boolean;
|
||||
public backgroundSpanUnderStatusBar: boolean;
|
||||
public hasActionBar: boolean;
|
||||
public accessibilityAnnouncePageEnabled = true;
|
||||
|
||||
get navigationContext(): any {
|
||||
return this._navigationContext;
|
||||
@ -126,8 +127,12 @@ export class PageBase extends ContentView {
|
||||
}
|
||||
|
||||
@profile
|
||||
public onNavigatedTo(isBackNavigation: boolean) {
|
||||
public onNavigatedTo(isBackNavigation: boolean): void {
|
||||
this.notify(this.createNavigatedData(PageBase.navigatedToEvent, isBackNavigation));
|
||||
|
||||
if (this.accessibilityAnnouncePageEnabled) {
|
||||
this.accessibilityScreenChanged(!!isBackNavigation);
|
||||
}
|
||||
}
|
||||
|
||||
@profile
|
||||
@ -152,6 +157,10 @@ export class PageBase extends ContentView {
|
||||
get _childrenCount(): number {
|
||||
return (this.content ? 1 : 0) + (this._actionBar ? 1 : 0);
|
||||
}
|
||||
|
||||
public accessibilityScreenChanged(refocus?: boolean): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PageBase.prototype.recycleNativeView = 'never';
|
||||
|
24
packages/core/ui/slider/index.d.ts
vendored
24
packages/core/ui/slider/index.d.ts
vendored
@ -1,10 +1,14 @@
|
||||
import { View } from '../core/view';
|
||||
import { Property, CoercibleProperty } from '../core/properties';
|
||||
import { EventData } from '../../data/observable';
|
||||
|
||||
/**
|
||||
* Represents a slider component.
|
||||
*/
|
||||
export class Slider extends View {
|
||||
static readonly accessibilityDecrementEvent = 'accessibilityDecrement';
|
||||
static readonly accessibilityIncrementEvent = 'accessibilityIncrement';
|
||||
|
||||
/**
|
||||
* Gets the native [android widget](http://developer.android.com/reference/android/widget/SeekBar.html) that represents the user interface for this component. Valid only when running on Android OS.
|
||||
*/
|
||||
@ -29,6 +33,11 @@ export class Slider extends View {
|
||||
* Gets or sets a slider max value. The default value is 100.
|
||||
*/
|
||||
maxValue: number;
|
||||
|
||||
/**
|
||||
* Increase/Decrease step size for iOS Increment-/Decrement events
|
||||
*/
|
||||
accessibilityStep: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,3 +54,18 @@ export const minValueProperty: Property<Slider, number>;
|
||||
* Represents the observable property backing the maxValue property of each Slider instance.
|
||||
*/
|
||||
export const maxValueProperty: CoercibleProperty<Slider, number>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the accessibilityStep property of each Slider instance.
|
||||
*/
|
||||
export const accessibilityStepProperty: Property<SliderBase, number>;
|
||||
|
||||
interface AccessibilityIncrementEventData extends EventData {
|
||||
object: Slider;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
interface AccessibilityDecrementEventData extends EventData {
|
||||
object: Slider;
|
||||
value?: number;
|
||||
}
|
||||
|
@ -3,9 +3,44 @@ import { Background } from '../styling/background';
|
||||
import { SliderBase, valueProperty, minValueProperty, maxValueProperty } from './slider-common';
|
||||
import { colorProperty, backgroundColorProperty, backgroundInternalProperty } from '../styling/style-properties';
|
||||
import { Color } from '../../color';
|
||||
import { AccessibilityDecrementEventData, AccessibilityIncrementEventData } from '.';
|
||||
|
||||
export * from './slider-common';
|
||||
|
||||
@NativeClass()
|
||||
class TNSSlider extends UISlider {
|
||||
public owner: WeakRef<Slider>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<Slider>) {
|
||||
const slider = TNSSlider.new() as TNSSlider;
|
||||
slider.owner = owner;
|
||||
|
||||
return slider;
|
||||
}
|
||||
|
||||
public accessibilityIncrement() {
|
||||
const owner = this.owner.get();
|
||||
if (!owner) {
|
||||
this.value += 10;
|
||||
} else {
|
||||
this.value = owner._handlerAccessibilityIncrementEvent();
|
||||
}
|
||||
|
||||
this.sendActionsForControlEvents(UIControlEvents.ValueChanged);
|
||||
}
|
||||
|
||||
public accessibilityDecrement() {
|
||||
const owner = this.owner.get();
|
||||
if (!owner) {
|
||||
this.value += 10;
|
||||
} else {
|
||||
this.value = owner._handlerAccessibilityDecrementEvent();
|
||||
}
|
||||
|
||||
this.sendActionsForControlEvents(UIControlEvents.ValueChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@NativeClass
|
||||
class SliderChangeHandlerImpl extends NSObject {
|
||||
private _owner: WeakRef<Slider>;
|
||||
@ -30,11 +65,11 @@ class SliderChangeHandlerImpl extends NSObject {
|
||||
}
|
||||
|
||||
export class Slider extends SliderBase {
|
||||
nativeViewProtected: UISlider;
|
||||
nativeViewProtected: TNSSlider;
|
||||
private _changeHandler: NSObject;
|
||||
|
||||
public createNativeView() {
|
||||
return UISlider.new();
|
||||
public createNativeView(): TNSSlider {
|
||||
return TNSSlider.initWithOwner(new WeakRef(this));
|
||||
}
|
||||
|
||||
public initNativeView(): void {
|
||||
@ -47,7 +82,7 @@ export class Slider extends SliderBase {
|
||||
nativeView.addTargetActionForControlEvents(this._changeHandler, 'sliderValueChanged', UIControlEvents.ValueChanged);
|
||||
}
|
||||
|
||||
public disposeNativeView() {
|
||||
public disposeNativeView(): void {
|
||||
this._changeHandler = null;
|
||||
super.disposeNativeView();
|
||||
}
|
||||
@ -98,4 +133,36 @@ export class Slider extends SliderBase {
|
||||
[backgroundInternalProperty.setNative](value: Background) {
|
||||
//
|
||||
}
|
||||
|
||||
private getAccessibilityStep(): number {
|
||||
if (!this.accessibilityStep || this.accessibilityStep <= 0) {
|
||||
return 10;
|
||||
}
|
||||
|
||||
return this.accessibilityStep;
|
||||
}
|
||||
|
||||
public _handlerAccessibilityIncrementEvent(): number {
|
||||
const args: AccessibilityIncrementEventData = {
|
||||
object: this,
|
||||
eventName: SliderBase.accessibilityIncrementEvent,
|
||||
value: this.value + this.getAccessibilityStep(),
|
||||
};
|
||||
|
||||
this.notify(args);
|
||||
|
||||
return args.value;
|
||||
}
|
||||
|
||||
public _handlerAccessibilityDecrementEvent(): number {
|
||||
const args: AccessibilityDecrementEventData = {
|
||||
object: this,
|
||||
eventName: SliderBase.accessibilityIncrementEvent,
|
||||
value: this.value - this.getAccessibilityStep(),
|
||||
};
|
||||
|
||||
this.notify(args);
|
||||
|
||||
return args.value;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,28 @@
|
||||
import { Slider as SliderDefinition } from '.';
|
||||
import { View, CSSType } from '../core/view';
|
||||
import { Property, CoercibleProperty } from '../core/properties';
|
||||
import { AccessibilityRole } from '../../accessibility';
|
||||
import { CoercibleProperty, Property } from '../core/properties';
|
||||
import { CSSType, View } from '../core/view';
|
||||
|
||||
// TODO: Extract base Range class for slider and progress
|
||||
@CSSType('Slider')
|
||||
export class SliderBase extends View implements SliderDefinition {
|
||||
static readonly accessibilityIncrementEvent = 'accessibilityIncrement';
|
||||
static readonly accessibilityDecrementEvent = 'accessibilityDecrement';
|
||||
|
||||
public value: number;
|
||||
public minValue: number;
|
||||
public maxValue: number;
|
||||
|
||||
get accessibilityStep(): number {
|
||||
return this.style.accessibilityStep;
|
||||
}
|
||||
|
||||
set accessibilityStep(value: number) {
|
||||
this.style.accessibilityStep = value;
|
||||
}
|
||||
|
||||
accessible = true;
|
||||
accessibilityRole = AccessibilityRole.Adjustable;
|
||||
}
|
||||
|
||||
SliderBase.prototype.recycleNativeView = 'auto';
|
||||
|
@ -15,7 +15,7 @@ export abstract class Font implements FontDefinition {
|
||||
return this.fontWeight === FontWeight.SEMI_BOLD || this.fontWeight === FontWeight.BOLD || this.fontWeight === '700' || this.fontWeight === FontWeight.EXTRA_BOLD || this.fontWeight === FontWeight.BLACK;
|
||||
}
|
||||
|
||||
protected constructor(public readonly fontFamily: string, public readonly fontSize: number, public readonly fontStyle: FontStyle, public readonly fontWeight: FontWeight) {}
|
||||
protected constructor(public readonly fontFamily: string, public readonly fontSize: number, public readonly fontStyle: FontStyle, public readonly fontWeight: FontWeight, public readonly fontScale: number) {}
|
||||
|
||||
public abstract getAndroidTypeface(): any /* android.graphics.Typeface */;
|
||||
public abstract getUIFont(defaultFont: any /* UIFont */): any /* UIFont */;
|
||||
@ -23,6 +23,7 @@ export abstract class Font implements FontDefinition {
|
||||
public abstract withFontStyle(style: string): Font;
|
||||
public abstract withFontWeight(weight: string): Font;
|
||||
public abstract withFontSize(size: number): Font;
|
||||
public abstract withFontScale(scale: number): Font;
|
||||
|
||||
public static equals(value1: Font, value2: Font): boolean {
|
||||
// both values are falsy
|
||||
|
@ -15,7 +15,7 @@ export class Font extends FontBase {
|
||||
private _typeface: android.graphics.Typeface;
|
||||
|
||||
constructor(family: string, size: number, style: 'normal' | 'italic', weight: FontWeight) {
|
||||
super(family, size, style, weight);
|
||||
super(family, size, style, weight, 1);
|
||||
}
|
||||
|
||||
public withFontFamily(family: string): Font {
|
||||
@ -34,6 +34,10 @@ export class Font extends FontBase {
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public withFontScale(scale: number): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, this.fontWeight);
|
||||
}
|
||||
|
||||
public getAndroidTypeface(): android.graphics.Typeface {
|
||||
if (!this._typeface) {
|
||||
this._typeface = createTypeface(this);
|
||||
|
2
packages/core/ui/styling/font.d.ts
vendored
2
packages/core/ui/styling/font.d.ts
vendored
@ -5,6 +5,7 @@
|
||||
public fontStyle: FontStyle;
|
||||
public fontWeight: FontWeight;
|
||||
public fontSize: number;
|
||||
public fontScale: number;
|
||||
|
||||
public isBold: boolean;
|
||||
public isItalic: boolean;
|
||||
@ -18,6 +19,7 @@
|
||||
public withFontStyle(style: string): Font;
|
||||
public withFontWeight(weight: string): Font;
|
||||
public withFontSize(size: number): Font;
|
||||
public withFontScale(scale: number): Font;
|
||||
|
||||
public static equals(value1: Font, value2: Font): boolean;
|
||||
}
|
||||
|
@ -11,28 +11,32 @@ const DEFAULT_MONOSPACE = 'Courier New';
|
||||
const SUPPORT_FONT_WEIGHTS = parseFloat(Device.osVersion) >= 10.0;
|
||||
|
||||
export class Font extends FontBase {
|
||||
public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL);
|
||||
public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL, 1);
|
||||
|
||||
private _uiFont: UIFont;
|
||||
|
||||
constructor(family: string, size: number, style: FontStyle, weight: FontWeight) {
|
||||
super(family, size, style, weight);
|
||||
constructor(family: string, size: number, style: FontStyle, weight: FontWeight, scale: number) {
|
||||
super(family, size, style, weight, scale);
|
||||
}
|
||||
|
||||
public withFontFamily(family: string): Font {
|
||||
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight);
|
||||
return new Font(family, this.fontSize, this.fontStyle, this.fontWeight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontStyle(style: FontStyle): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight);
|
||||
return new Font(this.fontFamily, this.fontSize, style, this.fontWeight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontWeight(weight: FontWeight): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight);
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, weight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontSize(size: number): Font {
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight);
|
||||
return new Font(this.fontFamily, size, this.fontStyle, this.fontWeight, this.fontScale);
|
||||
}
|
||||
|
||||
public withFontScale(scale: number): Font {
|
||||
return new Font(this.fontFamily, this.fontSize, this.fontStyle, this.fontWeight, scale);
|
||||
}
|
||||
|
||||
public getUIFont(defaultFont: UIFont): UIFont {
|
||||
|
@ -1422,6 +1422,27 @@ export const fontFamilyProperty = new InheritedCssProperty<Style, string>({
|
||||
});
|
||||
fontFamilyProperty.register(Style);
|
||||
|
||||
export const fontScaleProperty = new InheritedCssProperty<Style, number>({
|
||||
name: '_fontScale',
|
||||
cssName: '_fontScale',
|
||||
affectsLayout: global.isIOS,
|
||||
valueChanged: (target, oldValue, newValue) => {
|
||||
if (global.isIOS) {
|
||||
if (target.viewRef['handleFontSize'] === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFont = target.fontInternal || Font.default;
|
||||
if (currentFont.fontScale !== newValue) {
|
||||
const newFont = currentFont.withFontScale(newValue);
|
||||
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
|
||||
}
|
||||
}
|
||||
},
|
||||
valueConverter: (v) => parseFloat(v),
|
||||
});
|
||||
fontScaleProperty.register(Style);
|
||||
|
||||
export const fontSizeProperty = new InheritedCssProperty<Style, number>({
|
||||
name: 'fontSize',
|
||||
cssName: 'font-size',
|
||||
|
@ -11,6 +11,7 @@ import { Observable } from '../../../data/observable';
|
||||
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout';
|
||||
import { Trace } from '../../../trace';
|
||||
import { TextAlignment, TextDecoration, TextTransform, WhiteSpace, TextShadow } from '../../text-base';
|
||||
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
|
||||
import { BoxShadow } from '../box-shadow';
|
||||
|
||||
export interface CommonLayoutParams {
|
||||
@ -98,6 +99,7 @@ export class Style extends Observable implements StyleDefinition {
|
||||
}
|
||||
|
||||
public fontInternal: Font;
|
||||
public _fontScale: number;
|
||||
public backgroundInternal: Background;
|
||||
|
||||
public rotate: number;
|
||||
@ -211,6 +213,16 @@ export class Style extends Observable implements StyleDefinition {
|
||||
public flexWrapBefore: FlexWrapBefore;
|
||||
public alignSelf: AlignSelf;
|
||||
|
||||
// Accessibility properties
|
||||
public accessible: boolean;
|
||||
public accessibilityHidden: boolean;
|
||||
public accessibilityRole: AccessibilityRole;
|
||||
public accessibilityState: AccessibilityState;
|
||||
public accessibilityLiveRegion: AccessibilityLiveRegion;
|
||||
public accessibilityLanguage: string;
|
||||
public accessibilityMediaSession: boolean;
|
||||
public accessibilityStep: number;
|
||||
|
||||
public PropertyBag: {
|
||||
new (): { [property: string]: string };
|
||||
prototype: { [property: string]: string };
|
||||
|
Reference in New Issue
Block a user