chore: a11y polish (#9259)

This commit is contained in:
Nathan Walker
2021-03-08 10:26:01 -08:00
parent aaeab990c8
commit a822f2affb
29 changed files with 356 additions and 261 deletions

View File

@ -183,7 +183,19 @@ components that have the btn class name.
margin-bottom: 5
}
.a11y-item {
.a11y-demo-page .view-item {
margin-bottom: 12;
font-size: 18;
}
.a11y-demo-page .a11y {
a11y-enabled: true;
}
.a11y-demo-page .a11y-role-image {
a11y-role: image;
}
.a11y-demo-page .a11y-state-checked {
a11y-state: checked;
}

View File

@ -1,8 +1,34 @@
import { Observable, EventData, Page } from '@nativescript/core';
import { Observable, EventData, Page, Switch, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, ShowModalOptions } from '@nativescript/core';
let page: Page;
export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page = <Page>args.object;
page.bindingContext = new AccessibilityModel();
page.onAccessibilityPerformEscape = () => {
console.log('onAccessibilityPerformEscape');
return true;
};
}
export class AccessibilityModel extends Observable {}
export class AccessibilityModel extends Observable {
labelText = 'Label change on Switch:';
switchCheckedText = this.labelText;
accessibilityLiveRegions = AccessibilityLiveRegion;
accessibilityRole = AccessibilityRole;
accessibilityState = AccessibilityState;
constructor() {
super();
}
checkedChange(args) {
const checked = (args.object as Switch).checked;
console.log(checked);
this.set('switchCheckedText', `${this.labelText} ${checked}`);
}
openModal() {
page.showModal('pages/sample-modal');
}
}

View File

@ -4,14 +4,32 @@
</ActionBar>
</Page.actionBar>
<GridLayout padding="20">
<GridLayout padding="20" class="a11y-demo-page">
<ScrollView>
<StackLayout>
<Label text="Accessible Label" class="a11y-item text-center"></Label>
<Button text="Accessible Button" class="a11y-item"></Button>
<!-- <Label text="Accessible Label" accessible="true" class="a11y-item text-center"></Label>
<Button text="Accessible Button" accessible="true" class="a11y-item"></Button> -->
<Label text="Accessible Label" class="view-item a11y text-center" accessibilityLabel="Accessible Label" accessibilityHint="Just a label" accessibilityRole="{{accessibilityRole.StaticText}}" accessibilityValue="Accessible Label" />
<Button text="Accessible Button" class="view-item a11y" accessibilityLabel="Accessible Button" accessibilityHint="Tapping this really does nothing" />
<Image src="res://icon" width="50" class="view-item a11y" accessibilityLabel="Image with explicit attribute role" accessibilityRole="{{accessibilityRole.Image}}" />
<Image src="res://icon" width="50" class="view-item a11y a11y-role-image" accessibilityLabel="Image with css defined role" />
<Switch checked="true" class="view-item a11y" accessibilityLabel="Switch with attribute state" accessibilityState="{{accessibilityState.Checked}}" checkedChange="{{checkedChange}}" />
<Switch checked="true" class="view-item a11y a11y-state-checked" accessibilityLabel="Switch with css state" checkedChange="{{checkedChange}}" />
<TextView hint="TextView" text="{{switchCheckedText}}" class="view-item a11y" accessibilityLabel="TestView with a value" accessibilityLiveRegion="{{accessibilityLiveRegions.Polite}}"/>
<TextField hint="TextField" class="view-item a11y" accessibilityLabel="Plain jane TextField" accessibilityHint="Tell us your real name Jane"/>
<TextView hint="TextView" class="view-item a11y" accessibilityLabel="Nice TextView" accessibilityHint="Tell us about yourself Jane"/>
<GridLayout rows="25" columns="*" class="view-item" accessibilityLabel="No can go GridLayout" accessibilityHint="A grid that will not get bigger when increasing accessible text size">
<Label text="IN-Accessible Grid" class="view-item text-center" />
</GridLayout>
<GridLayout rows="25,25" columns="*,50" class="view-item a11y" accessibilityLabel="Yes an accessible GridLayout" accessibilityHint="A grid that WILL get bigger dynamically when increasing accessible text size">
<Label text="Accessible Grid" class="view-item text-center" />
<Label row="1" text="With another item in a row" class="view-item text-center" />
<Label rowSpan="2" col="1" text="Hi" />
</GridLayout>
<Button text="Open Modal" class="view-item" tap="{{openModal}}" />
<Slider value="10" minValue="0" maxValue="100" class="view-item a11y" accessibilityLabel="Slider" accessibilityHint="A smooth slider" accessibilityValue="10"/>
</StackLayout>
</ScrollView>
</GridLayout>
</Page>
</Page>

View File

@ -0,0 +1,23 @@
import { Page, ShownModallyData, Observable } from '@nativescript/core';
let page: Page;
let closeCallback: Function;
export function onShownModally(args: ShownModallyData) {
page = <Page>args.object;
page.bindingContext = new SampleModal();
closeCallback = args.closeCallback;
if (args.context) {
args.context.shownModally = true;
}
}
export class SampleModal extends Observable {
close() {
// TODO: a11y
// if (global.isIOS) {
// (<UIViewController>page.ios).view.accessibilityPerformEscape();
// }
closeCallback();
}
}

View File

@ -0,0 +1,11 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" shownModally="onShownModally" class="page">
<GridLayout padding="20">
<ScrollView>
<StackLayout>
<Button text="Close" class="view-item" tap="{{close}}" />
</StackLayout>
</ScrollView>
</GridLayout>
</Page>

View File

@ -29,6 +29,7 @@
"@nrwl/workspace": "11.4.0",
"@nstudio/focus": "~11.1.0",
"@nstudio/nps-i": "~1.1.0",
"@prettier/plugin-xml": "^0.13.1",
"@types/chai": "^4.2.11",
"@types/jest": "~26.0.8",
"@types/mocha": "^7.0.2",

View File

@ -1,12 +1,13 @@
import { EventData, EventDataValue } from '../data/observable';
import type { View } from '../ui/core/view';
import type { Page } from '../ui/page';
import type { AccessibilityBlurEventData, AccessibilityFocusChangedEventData, AccessibilityFocusEventData } from './accessibility-types';
const lastFocusedViewOnPageKeyName = '__lastFocusedViewOnPage';
export const accessibilityBlurEvent = 'accessibilityBlur';
export const accessibilityFocusEvent = 'accessibilityFocus';
export const accessibilityFocusChangedEvent = 'accessibilityFocusChanged';
export const accessibilityPerformEscapeEvent = 'accessibilityPerformEscape';
/**
* Send notification when accessibility focus state changes.
@ -18,7 +19,7 @@ export const accessibilityFocusChangedEvent = 'accessibilityFocusChanged';
* @param {boolean} receivedFocus
* @param {boolean} lostFocus
*/
export function notifyAccessibilityFocusState(view: View, receivedFocus: boolean, lostFocus: boolean): void {
export function notifyAccessibilityFocusState(view: Partial<View>, receivedFocus: boolean, lostFocus: boolean): void {
if (!receivedFocus && !lostFocus) {
return;
}
@ -27,7 +28,7 @@ export function notifyAccessibilityFocusState(view: View, receivedFocus: boolean
eventName: accessibilityFocusChangedEvent,
object: view,
value: !!receivedFocus,
} as AccessibilityFocusChangedEventData);
} as EventDataValue);
if (receivedFocus) {
if (view.page) {
@ -37,12 +38,12 @@ export function notifyAccessibilityFocusState(view: View, receivedFocus: boolean
view.notify({
eventName: accessibilityFocusEvent,
object: view,
} as AccessibilityFocusEventData);
} as EventData);
} else if (lostFocus) {
view.notify({
eventName: accessibilityBlurEvent,
object: view,
} as AccessibilityBlurEventData);
} as EventData);
}
}

View File

@ -24,16 +24,12 @@ function makePropertyEnumConverter<T>(enumValues) {
};
}
export const accessibilityEnabledProperty = new Property<View, boolean>({
export const accessibilityEnabledProperty = new CssProperty<Style, boolean>({
name: 'accessible',
// cssName: 'a11y-enabled',
defaultValue: false,
valueConverter: (v) => {
console.log('accessibilityEnabledProperty:', v);
return booleanConverter(v);
},
cssName: 'a11y-enabled',
valueConverter: booleanConverter,
});
// accessibilityEnabledProperty.register(Style);
accessibilityEnabledProperty.register(Style);
const accessibilityHiddenPropertyName = 'accessibilityHidden';
const accessibilityHiddenCssName = 'a11y-hidden';
@ -81,6 +77,11 @@ export const accessibilityHintProperty = new Property<View, string>({
name: 'accessibilityHint',
});
export const accessibilityIgnoresInvertColorsProperty = new Property<View, boolean>({
name: 'accessibilityIgnoresInvertColors',
valueConverter: booleanConverter,
});
export const accessibilityLiveRegionProperty = new CssProperty<Style, AccessibilityLiveRegion>({
name: 'accessibilityLiveRegion',
cssName: 'a11y-live-region',

View File

@ -30,8 +30,8 @@ class AndroidSharedA11YObservable extends SharedA11YObservable {
}
}
let accessibilityStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.AccessibilityStateChangeListener;
let touchExplorationStateChangeListener: androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
let accessibilityStateChangeListener: android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
let touchExplorationStateChangeListener: android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
let sharedA11YObservable: AndroidSharedA11YObservable;
function updateAccessibilityState(): void {
@ -72,7 +72,7 @@ function ensureStateListener(): SharedA11YObservable {
},
});
touchExplorationStateChangeListener = new androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener({
touchExplorationStateChangeListener = new android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener({
onTouchExplorationStateChanged(enabled) {
updateAccessibilityState();
@ -83,7 +83,7 @@ function ensureStateListener(): SharedA11YObservable {
});
accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
androidx.core.view.accessibility.AccessibilityManagerCompat.addTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
updateAccessibilityState();
@ -109,7 +109,7 @@ Application.on(Application.exitEvent, (args: Application.ApplicationEventData) =
}
if (touchExplorationStateChangeListener) {
androidx.core.view.accessibility.AccessibilityManagerCompat.removeTouchExplorationStateChangeListener(accessibilityManager, touchExplorationStateChangeListener);
accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationStateChangeListener);
}
}

View File

@ -1,77 +1,6 @@
import type { EventData } from '../data/observable';
import type { View } from '../ui/core/view';
export enum AccessibilityTrait {
/**
* The element has no traits.
*/
None = 'none',
/**
* The element should be treated as a button.
*/
Button = 'button',
/**
* The element should be treated as a link.
*/
Link = 'link',
/**
* The element should be treated as a search field.
*/
SearchField = 'search',
/**
* The element should be treated as an image.
*/
Image = 'image',
/**
* The element is currently selected.
*/
Selected = 'selected',
/**
* The element plays its own sound when activated.
*/
PlaysSound = 'plays',
/**
* The element behaves as a keyboard key.
*/
KeyboardKey = 'key',
/**
* The element should be treated as static text that cannot change.
*/
StaticText = 'text',
/**
* The element provides summary information when the application starts.
*/
SummaryElement = 'summary',
/**
* The element is not enabled and does not respond to user interaction.
*/
NotEnabled = 'disabled',
/**
* The element frequently updates its label or value.
*/
UpdatesFrequently = 'frequentUpdates',
/**
* The element starts a media session when it is activated.
*/
StartsMediaSession = 'startsMedia',
/**
* The element allows continuous adjustment through a range of values.
*/
Adjustable = 'adjustable',
/**
* The element allows direct touch interaction for VoiceOver users.
*/
@ -84,16 +13,26 @@ export enum AccessibilityTrait {
CausesPageTurn = 'pageTurn',
/**
* The element is a header that divides content into sections, such as the title of a navigation bar.
* The element is not enabled and does not respond to user interaction.
*/
Header = 'header',
NotEnabled = 'disabled',
/**
* The element is currently selected.
*/
Selected = 'selected',
/**
* The element frequently updates its label or value.
*/
UpdatesFrequently = 'frequentUpdates',
}
export enum AccessibilityRole {
/**
* The element has no traits.
* The element allows continuous adjustment through a range of values.
*/
None = 'none',
Adjustable = 'adjustable',
/**
* The element should be treated as a button.
@ -101,14 +40,14 @@ export enum AccessibilityRole {
Button = 'button',
/**
* The element should be treated as a link.
* The element behaves like a Checkbox
*/
Link = 'link',
Checkbox = 'checkbox',
/**
* The element should be treated as a search field.
* The element is a header that divides content into sections, such as the title of a navigation bar.
*/
Search = 'search',
Header = 'header',
/**
* The element should be treated as an image.
@ -126,29 +65,19 @@ export enum AccessibilityRole {
KeyboardKey = 'keyboardKey',
/**
* The element should be treated as static text that cannot change.
* The element should be treated as a link.
*/
StaticText = 'textField',
Link = 'link',
/**
* The element allows continuous adjustment through a range of values.
* The element has no traits.
*/
Adjustable = 'adjustable',
None = 'none',
/**
* The element provides summary information when the application starts.
* The element plays its own sound when activated.
*/
Summary = 'summary',
/**
* The element is a header that divides content into sections, such as the title of a navigation bar.
*/
Header = 'header',
/**
* The element behaves like a Checkbox
*/
Checkbox = 'checkbox',
PlaysSound = 'plays',
/**
* The element behaves like a ProgressBar
@ -160,11 +89,31 @@ export enum AccessibilityRole {
*/
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
*/
@ -184,16 +133,6 @@ export enum AccessibilityLiveRegion {
Assertive = 'assertive',
}
export interface AccessibilityFocusEventData extends EventData {
object: View;
}
export type AccessibilityBlurEventData = AccessibilityFocusEventData;
export interface AccessibilityFocusChangedEventData extends AccessibilityFocusEventData {
value: boolean;
}
export enum IOSPostAccessibilityNotificationType {
Announcement = 'announcement',
Screen = 'screen',
@ -324,3 +263,13 @@ export enum AndroidAccessibilityEvent {
*/
ALL_MASK = 'all',
}
export interface AccessibilityEventPerformEscape extends EventData {
cancel?: boolean;
}
export interface AccessibilityEventOptions {
androidAccessibilityEvent?: AndroidAccessibilityEvent;
iosNotificationType?: IOSPostAccessibilityNotificationType;
message?: string;
}

View File

@ -12,8 +12,8 @@ export * from './font-scale';
let clickableRolesMap = new Set<string>();
let lastFocusedView: WeakRef<View>;
function accessibilityEventHelper(view: View, eventType: number) {
let lastFocusedView: WeakRef<Partial<View>>;
function accessibilityEventHelper(view: Partial<View>, eventType: number) {
const eventName = accessibilityEventTypeMap.get(eventType);
if (!isAccessibilityServiceEnabled()) {
if (Trace.isEnabled()) {
@ -102,7 +102,7 @@ function accessibilityEventHelper(view: View, eventType: number) {
let TNSAccessibilityDelegate: android.view.View.androidviewViewAccessibilityDelegate;
const androidViewToTNSView = new WeakMap<android.view.View, WeakRef<View>>();
const androidViewToTNSView = new WeakMap<android.view.View, WeakRef<Partial<View>>>();
let accessibilityEventMap: Map<AndroidAccessibilityEvent, number>;
let accessibilityEventTypeMap: Map<number, string>;
@ -437,11 +437,11 @@ export function isAccessibilityServiceEnabled(): boolean {
return accessibilityServiceEnabled;
}
export function setupAccessibleView(view: View): void {
export function setupAccessibleView(view: Partial<View>): void {
updateAccessibilityProperties(view);
}
export function updateAccessibilityProperties(view: View): void {
export function updateAccessibilityProperties(view: Partial<View>): void {
if (!view.nativeViewProtected) {
return;
}
@ -537,7 +537,7 @@ export function updateContentDescription(view: View, forceUpdate?: boolean): str
return applyContentDescription(view, forceUpdate);
}
function setAccessibilityDelegate(view: View): void {
function setAccessibilityDelegate(view: Partial<View>): void {
if (!view.nativeViewProtected) {
return;
}
@ -551,7 +551,10 @@ function setAccessibilityDelegate(view: View): void {
androidViewToTNSView.set(androidView, new WeakRef(view));
const hasOldDelegate = androidView.getAccessibilityDelegate() === TNSAccessibilityDelegate;
let hasOldDelegate = false;
if (typeof androidView.getAccessibilityDelegate === 'function') {
hasOldDelegate = androidView.getAccessibilityDelegate() === TNSAccessibilityDelegate;
}
if (hasOldDelegate) {
return;
@ -560,7 +563,7 @@ function setAccessibilityDelegate(view: View): void {
androidView.setAccessibilityDelegate(TNSAccessibilityDelegate);
}
function applyContentDescription(view: View, forceUpdate?: boolean) {
function applyContentDescription(view: Partial<View>, forceUpdate?: boolean) {
let androidView = view.nativeViewProtected as android.view.View;
if (!androidView) {
return;

View File

@ -1,6 +1,6 @@
import type { Page } from '../ui/page';
import { View } from '../ui/core/view';
import { AndroidAccessibilityEvent } from './accessibility-types';
import type { View } from '../ui/core/view';
import type { AndroidAccessibilityEvent } from './accessibility-types';
export * from './accessibility-common';
export * from './accessibility-types';
@ -9,7 +9,7 @@ export * from './font-scale';
/**
* Initialize accessibility for View. This should be called on loaded-event.
*/
export function setupAccessibleView(view: View): void;
export function setupAccessibleView(view: Partial<View>): void;
/**
* Update accessibility properties on nativeView
@ -17,12 +17,12 @@ export function setupAccessibleView(view: View): void;
export function updateAccessibilityProperties(view: View): void;
/**
* Android helper function for triggering accessibility events
* Android: helper function for triggering accessibility events
*/
export function sendAccessibilityEvent(View: View, eventName: AndroidAccessibilityEvent, text?: string): void;
/**
* Update the content description for android views
* Android: Update the content description for views
*/
export function updateContentDescription(View: View, forceUpdate?: boolean): string | null;

View File

@ -46,38 +46,30 @@ function ensureNativeClasses() {
}
AccessibilityTraitsMap = new Map<AccessibilityTrait, number>([
[AccessibilityTrait.None, UIAccessibilityTraitNone],
[AccessibilityTrait.Button, UIAccessibilityTraitButton],
[AccessibilityTrait.Link, UIAccessibilityTraitLink],
[AccessibilityTrait.SearchField, UIAccessibilityTraitSearchField],
[AccessibilityTrait.Image, UIAccessibilityTraitImage],
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
[AccessibilityTrait.PlaysSound, UIAccessibilityTraitPlaysSound],
[AccessibilityTrait.StaticText, UIAccessibilityTraitStaticText],
[AccessibilityTrait.SummaryElement, UIAccessibilityTraitSummaryElement],
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
[AccessibilityTrait.StartsMediaSession, UIAccessibilityTraitStartsMediaSession],
[AccessibilityTrait.Adjustable, UIAccessibilityTraitAdjustable],
[AccessibilityTrait.AllowsDirectInteraction, UIAccessibilityTraitAllowsDirectInteraction],
[AccessibilityTrait.CausesPageTurn, UIAccessibilityTraitCausesPageTurn],
[AccessibilityTrait.Header, UIAccessibilityTraitHeader],
[AccessibilityTrait.NotEnabled, UIAccessibilityTraitNotEnabled],
[AccessibilityTrait.Selected, UIAccessibilityTraitSelected],
[AccessibilityTrait.UpdatesFrequently, UIAccessibilityTraitUpdatesFrequently],
]);
RoleTypeMap = new Map<AccessibilityRole, number>([
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
[AccessibilityRole.Button, UIAccessibilityTraitButton],
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
[AccessibilityRole.Header, UIAccessibilityTraitHeader],
[AccessibilityRole.Link, UIAccessibilityTraitLink],
[AccessibilityRole.Search, UIAccessibilityTraitSearchField],
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
[AccessibilityRole.Image, UIAccessibilityTraitImage],
[AccessibilityRole.ImageButton, UIAccessibilityTraitImage | UIAccessibilityTraitButton],
[AccessibilityRole.KeyboardKey, UIAccessibilityTraitKeyboardKey],
[AccessibilityRole.StaticText, UIAccessibilityTraitStaticText],
[AccessibilityRole.Summary, UIAccessibilityTraitSummaryElement],
[AccessibilityRole.Adjustable, UIAccessibilityTraitAdjustable],
[AccessibilityRole.Checkbox, UIAccessibilityTraitButton],
[AccessibilityRole.Switch, UIAccessibilityTraitButton],
[AccessibilityRole.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) => {
@ -159,6 +151,11 @@ export function updateAccessibilityProperties(view: View): void {
return;
}
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)) {
@ -198,15 +195,23 @@ export function updateAccessibilityProperties(view: View): void {
break;
}
}
if (view.accessibilityLiveRegion) {
console.log('accessibilityLiveRegion:', view.accessibilityLiveRegion);
}
if (view.accessibilityMediaSession) {
a11yTraits |= AccessibilityTraitsMap.get(AccessibilityTrait.StartsMediaSession);
}
if (view.accessibilityTraits) {
a11yTraits |= inputArrayToBitMask(view.accessibilityTraits, AccessibilityTraitsMap);
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);
// }
console.log('a11yTraits:', a11yTraits);
console.log(' ');
uiView.accessibilityTraits = a11yTraits;
}

View File

@ -2,12 +2,16 @@ import { Observable as ObservableDefinition, WrappedValue as WrappedValueDefinit
export interface EventData {
eventName: string;
object: Observable;
object: Partial<Observable>;
}
export interface EventDataValue extends EventData {
value?: boolean;
}
export interface NotifyData extends Partial<EventData> {
eventName: string;
object?: Observable;
object?: Partial<Observable>;
}
export interface PropertyChangeData extends EventData {

View File

@ -56,6 +56,12 @@ export declare const ApplicationSettings: {
getNumber: typeof getNumber;
setNumber: typeof setNumber;
};
export declare const AccessibilityEvents: {
accessibilityBlurEvent: string;
accessibilityFocusEvent: string;
accessibilityFocusChangedEvent: string;
};
export { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, FontScaleCategory } from './accessibility';
export { Color } from './color';
import { connectionType, getConnectionType, startMonitoring, stopMonitoring } from './connectivity';
export declare const Connectivity: {

View File

@ -58,6 +58,15 @@ export const ApplicationSettings = {
setNumber,
};
import { accessibilityBlurEvent, accessibilityFocusEvent, accessibilityFocusChangedEvent, accessibilityPerformEscapeEvent } from './accessibility';
export const AccessibilityEvents = {
accessibilityBlurEvent,
accessibilityFocusEvent,
accessibilityFocusChangedEvent,
accessibilityPerformEscapeEvent,
};
export { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, FontScaleCategory } from './accessibility';
export { Color } from './color';
import { connectionType, getConnectionType, startMonitoring, stopMonitoring } from './connectivity';

View File

@ -8,7 +8,7 @@ import { AccessibilityRole } from '../../accessibility';
export abstract class ButtonBase extends TextBase implements ButtonDefinition {
public static tapEvent = 'tap';
// accessible = true;
accessible = true;
accessibilityRole = AccessibilityRole.Button;
get textWrap(): boolean {

View File

@ -6,7 +6,7 @@ import { Style } from '../../styling/style';
import { Page } from '../../page';
import { Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout';
import { Length } from '../../styling/style-properties';
import { Length, LengthType } from '../../styling/style-properties';
import { DOMNode } from '../../../debugger/dom-node';
/**
@ -105,8 +105,8 @@ export interface ShowModalOptions {
export abstract class ViewBase extends Observable {
// Dynamic properties.
left: Length;
top: Length;
left: LengthType;
top: LengthType;
effectiveLeft: number;
effectiveTop: number;
dock: 'left' | 'top' | 'right' | 'bottom';

View File

@ -815,7 +815,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this._context = context;
// This will account for nativeView that is created in createNativeView, recycled
// or for backward compatability - set before _setupUI in iOS contructor.
// or for backward compatibility - set before _setupUI in iOS constructor.
let nativeView = this.nativeViewProtected;
// if (global.isAndroid) {

View File

@ -21,7 +21,7 @@ import { AndroidActivityBackPressedEventData, android as androidApp } from '../.
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 { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, setupAccessibleView, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
import * as Utils from '../../../utils';
export * from './view-common';
@ -782,6 +782,7 @@ export class View extends ViewCommon {
}
[accessibilityRoleProperty.setNative](value: AccessibilityRole): void {
this.accessibilityRole = value;
updateAccessibilityProperties(this);
if (android.os.Build.VERSION.SDK_INT >= 28) {
@ -829,7 +830,8 @@ export class View extends ViewCommon {
}
}
[accessibilityStateProperty.setNative](): void {
[accessibilityStateProperty.setNative](value: AccessibilityState): void {
this.accessibilityState = value;
updateAccessibilityProperties(this);
}
@ -1102,18 +1104,10 @@ export class View extends ViewCommon {
}
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);
}
}

View File

@ -6,7 +6,7 @@ import { Animation, AnimationDefinition, AnimationPromise } from '../../animatio
import { LengthType, PercentLengthType } from '../../styling/style-properties';
import { GestureTypes, GesturesObserver } from '../../gestures';
import { LinearGradient } from '../../styling/gradient';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AndroidAccessibilityEvent, IOSPostAccessibilityNotificationType } from '../../../accessibility/accessibility-types';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AccessibilityEventOptions } from '../../../accessibility/accessibility-types';
import { Enums } from '../../enums';
import { CSSShadow } from '../../styling/css-shadow';
@ -286,7 +286,6 @@ export abstract class View extends ViewBase {
* A hint describes the elements behavior. Example: 'Tap change playback speed'
*/
accessibilityHint: string;
accessibilityTraits?: AccessibilityTrait[];
accessibilityLiveRegion: AccessibilityLiveRegion;
/**
@ -750,17 +749,18 @@ export abstract class View extends ViewBase {
public eachChildView(callback: (view: View) => boolean): void;
/**
* Android: Send accessibility event
* Send accessibility event
* @params options AccessibilityEventOptions
* androidAccessibilityEvent: AndroidAccessibilityEvent;
* iosNotificationType: IOSPostAccessibilityNotificationType;
* message: string;
*
* iOS Notes:
* 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 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;
public sendAccessibilityEvent(options: Partial<AccessibilityEventOptions>): void;
/**
* Make an announcement to the screen reader.

View File

@ -10,8 +10,8 @@ import { IOSHelper } from './view-helper';
import { ios as iosBackground, Background } from '../../styling/background';
import { perspectiveProperty, 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';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { setupAccessibleView, IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
import { Enums } from '../../enums';
export * from './view-common';
@ -504,6 +504,13 @@ export class View extends ViewCommon implements ViewDefinition {
}
this._modalAnimatedOptions.push(animated);
// TODO: a11y
// controller.accessibilityViewIsModal = true;
// controller.accessibilityPerformEscape = () => {
// console.log('accessibilityPerformEscape!!')
// return true;
// }
parentController.presentViewControllerAnimatedCompletion(controller, animated, null);
const transitionCoordinator = parentController.transitionCoordinator;
if (transitionCoordinator) {
@ -575,11 +582,8 @@ export class View extends ViewCommon implements ViewDefinition {
this.nativeViewProtected.accessibilityIdentifier = value;
}
[accessibilityRoleProperty.setNative](): void {
updateAccessibilityProperties(this);
}
[accessibilityTraitsProperty.setNative](): void {
[accessibilityRoleProperty.setNative](value: AccessibilityRole): void {
this.accessibilityRole = value;
updateAccessibilityProperties(this);
}
@ -590,7 +594,12 @@ export class View extends ViewCommon implements ViewDefinition {
[accessibilityLabelProperty.setNative](value: string): void {
value = value == null ? null : `${value}`;
// not sure if needed for Label:
// if ((<any>this).nativeTextViewProtected) {
// (<any>this).nativeTextViewProtected.accessibilityLabel = value;
// } else {
this.nativeViewProtected.accessibilityLabel = value;
// }
}
[accessibilityHintProperty.setNative](value: string): void {
@ -598,6 +607,11 @@ export class View extends ViewCommon implements ViewDefinition {
this.nativeViewProtected.accessibilityHint = value;
}
[accessibilityIgnoresInvertColorsProperty.setNative](value: boolean) {
console.log('accessibilityIgnoresInvertColorsProperty:', !!value);
this.nativeViewProtected.accessibilityIgnoresInvertColors = !!value;
}
[accessibilityLanguageProperty.setNative](value: string): void {
value = value == null ? null : `${value}`;
this.nativeViewProtected.accessibilityLanguage = value;
@ -613,7 +627,8 @@ export class View extends ViewCommon implements ViewDefinition {
updateAccessibilityProperties(this);
}
[accessibilityStateProperty.setNative](): void {
[accessibilityStateProperty.setNative](value: AccessibilityState): void {
this.accessibilityState = value;
updateAccessibilityProperties(this);
}
@ -733,8 +748,12 @@ export class View extends ViewCommon implements ViewDefinition {
}
}
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void {
if (!notificationType) {
public sendAccessibilityEvent(options: Partial<AccessibilityEventOptions>): void {
if (!isAccessibilityServiceEnabled()) {
return;
}
if (!options.iosNotificationType) {
return;
}
@ -744,7 +763,7 @@ export class View extends ViewCommon implements ViewDefinition {
args = msg;
}
switch (notificationType) {
switch (options.iosNotificationType) {
case IOSPostAccessibilityNotificationType.Announcement: {
notification = UIAccessibilityAnnouncementNotification;
break;
@ -766,19 +785,16 @@ export class View extends ViewCommon implements ViewDefinition {
}
public accessibilityAnnouncement(msg = this.accessibilityLabel): void {
if (!isAccessibilityServiceEnabled()) {
return;
}
this.iosPostAccessibilityNotification(IOSPostAccessibilityNotificationType.Announcement, msg);
this.sendAccessibilityEvent({
iosNotificationType: IOSPostAccessibilityNotificationType.Announcement,
message: msg,
});
}
public accessibilityScreenChanged(): void {
if (!isAccessibilityServiceEnabled()) {
return;
}
this.iosPostAccessibilityNotification(IOSPostAccessibilityNotificationType.Screen);
this.sendAccessibilityEvent({
iosNotificationType: IOSPostAccessibilityNotificationType.Screen,
});
}
_getCurrentLayoutBounds(): {

View File

@ -22,9 +22,9 @@ import { StyleScope } from '../../styling/style-scope';
import { LinearGradient } from '../../styling/linear-gradient';
import * as am from '../../animation';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AndroidAccessibilityEvent, IOSPostAccessibilityNotificationType } from '../../../accessibility/accessibility-types';
import { accessibilityEnabledProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityTraitsProperty, accessibilityValueProperty } from '../../../accessibility/accessibility-properties';
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, getCurrentFontScale } from '../../../accessibility';
import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from '../../../accessibility/accessibility-types';
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent, getCurrentFontScale } from '../../../accessibility';
import { CSSShadow } from '../../styling/css-shadow';
// helpers (these are okay re-exported here)
@ -74,6 +74,12 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public static accessibilityBlurEvent = accessibilityBlurEvent;
public static accessibilityFocusEvent = accessibilityFocusEvent;
public static accessibilityFocusChangedEvent = accessibilityFocusChangedEvent;
public static accessibilityPerformEscapeEvent = accessibilityPerformEscapeEvent;
public accessibilityIdentifier: string;
public accessibilityLabel: string;
public accessibilityValue: string;
public accessibilityHint: string;
protected _closeModalCallback: Function;
public _manager: any;
@ -99,9 +105,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
_androidContentDescriptionUpdated?: boolean;
// a11y
_accessible: boolean;
get css(): string {
const scope = this._styleScope;
@ -757,12 +760,12 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
get accessible(): boolean {
// return this.style.accessible;
return this._accessible;
return this.style.accessible;
// return this._accessible;
}
set accessible(value: boolean) {
// this.style.accessible = value;
this._accessible = value;
this.style.accessible = value;
// this._accessible = value;
}
get accessibilityHidden(): boolean {
@ -772,8 +775,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style.accessibilityHidden = value;
}
public accessibilityIdentifier: string;
get accessibilityRole(): AccessibilityRole {
return this.style.accessibilityRole;
}
@ -788,10 +789,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style.accessibilityState = value;
}
public accessibilityLabel: string;
public accessibilityValue: string;
public accessibilityHint: string;
get accessibilityLiveRegion(): AccessibilityLiveRegion {
return this.style.accessibilityLiveRegion;
}
@ -813,8 +810,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style.accessibilityMediaSession = value;
}
public accessibilityTraits?: AccessibilityTrait[];
get automationText(): string {
return this.accessibilityIdentifier;
}
@ -1093,11 +1088,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return false;
}
public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void {
return;
}
public iosPostAccessibilityNotification(notificationType: IOSPostAccessibilityNotificationType, msg?: string): void {
public sendAccessibilityEvent(options: Partial<AccessibilityEventOptions>): void {
return;
}
@ -1160,9 +1151,8 @@ export const iosIgnoreSafeAreaProperty = new InheritedProperty({
valueConverter: booleanConverter,
});
iosIgnoreSafeAreaProperty.register(ViewCommon);
accessibilityEnabledProperty.register(ViewCommon);
accessibilityIdentifierProperty.register(ViewCommon);
accessibilityLabelProperty.register(ViewCommon);
accessibilityValueProperty.register(ViewCommon);
accessibilityHintProperty.register(ViewCommon);
accessibilityTraitsProperty.register(ViewCommon);
accessibilityIgnoresInvertColorsProperty.register(ViewCommon);

View File

@ -46,21 +46,21 @@ export namespace IOSHelper {
* Returns a view with viewController or undefined if no such found along the view's parent chain.
* @param view The view form which to start the search.
*/
export function getParentWithViewController(view: View): View;
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void;
export function updateConstraints(controller: any /* UIViewController */, owner: View): void;
export function layoutView(controller: any /* UIViewController */, owner: View): void;
export function getParentWithViewController(view: Partial<View>): View;
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: Partial<View>): void;
export function updateConstraints(controller: any /* UIViewController */, owner: Partial<View>): void;
export function layoutView(controller: any /* UIViewController */, owner: Partial<View>): void;
export function getPositionFromFrame(frame: any /* CGRect */): { left; top; right; bottom };
export function getFrameFromPosition(position: { left; top; right; bottom }, insets?: { left; top; right; bottom }): any; /* CGRect */
export function shrinkToSafeArea(view: View, frame: any /* CGRect */): any; /* CGRect */
export function expandBeyondSafeArea(view: View, frame: any /* CGRect */): any; /* CGRect */
export function shrinkToSafeArea(view: Partial<View>, frame: any /* CGRect */): any; /* CGRect */
export function expandBeyondSafeArea(view: Partial<View>, frame: any /* CGRect */): any; /* CGRect */
export class UILayoutViewController {
public static initWithOwner(owner: WeakRef<View>): UILayoutViewController;
public static initWithOwner(owner: WeakRef<Partial<View>>): UILayoutViewController;
}
export class UIAdaptivePresentationControllerDelegateImp {
public static initWithOwnerAndCallback(owner: WeakRef<View>, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp;
public static initWithOwnerAndCallback(owner: WeakRef<Partial<View>>, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp;
}
export class UIPopoverPresentationControllerDelegateImp {
public static initWithOwnerAndCallback(owner: WeakRef<View>, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp;
public static initWithOwnerAndCallback(owner: WeakRef<Partial<View>>, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp;
}
}

View File

@ -1,7 +1,7 @@
import { Observable, EventData } from '../../../data/observable';
const handlersForEventName = new Map<string, (eventData: EventData) => void>();
const sourcesMap = new WeakMap<Observable, Map<string, Array<TargetHandlerPair>>>();
const sourcesMap = new WeakMap<Partial<Observable>, Map<string, Array<TargetHandlerPair>>>();
class TargetHandlerPair {
tagetRef: WeakRef<Object>;

View File

@ -119,7 +119,7 @@ export interface GestureEventData extends EventData {
/**
* Gets the view which originates the gesture.
*/
view: View;
view: Partial<View>;
/**
* Gets the underlying native iOS specific [UIGestureRecognizer](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIGestureRecognizer_Class/).
*/
@ -266,7 +266,7 @@ export class GesturesObserver {
* @param callback - A function that will be executed when a gesture is received.
* @param context - default this argument for the callbacks.
*/
constructor(target: View, callback: (args: GestureEventData) => void, context: any);
constructor(target: Partial<View>, callback: (args: GestureEventData) => void, context: any);
/**
* Registers a gesture observer to a view and gesture.

View File

@ -99,6 +99,12 @@ export declare class Page extends PageBase {
// @ts-ignore
public actionBar: ActionBar;
/**
* iOS Only
* Perform an action when user performans the "escape" gesture
*/
public onAccessibilityPerformEscape?: () => boolean;
/**
* Should page changed be annnounced to the screen reader.
*/

View File

@ -66,6 +66,10 @@ function isBackNavigationFrom(controller: UIViewControllerImpl, page: Page): boo
@NativeClass
class UIViewControllerImpl extends UIViewController {
// TODO: a11y
// static ObjCExposedMethods = {
// accessibilityPerformEscape: { returns: interop.types.bool, params: [interop.types.void] },
// };
private _owner: WeakRef<Page>;
public isBackstackSkipped: boolean;
@ -311,6 +315,21 @@ class UIViewControllerImpl extends UIViewController {
}
}
// TODO: a11y
// public accessibilityPerformEscape() {
// const owner = this._owner.get();
// if (!owner) {
// return false;
// }
// console.log('page accessibilityPerformEscape');
// if (owner.onAccessibilityPerformEscape) {
// const result = owner.onAccessibilityPerformEscape();
// return result;
// } else {
// return false;
// }
// }
// @ts-ignore
public get preferredStatusBarStyle(): UIStatusBarStyle {
const owner = this._owner.get();
@ -325,6 +344,7 @@ class UIViewControllerImpl extends UIViewController {
export class Page extends PageBase {
nativeViewProtected: UIView;
viewController: UIViewControllerImpl;
onAccessibilityPerformEscape: () => boolean;
private _backgroundColor = majorVersion <= 12 && !UIColor.systemBackgroundColor ? UIColor.whiteColor : UIColor.systemBackgroundColor;
private _ios: UIViewControllerImpl;

View File

@ -21,7 +21,7 @@ export class SliderBase extends View implements SliderDefinition {
this.style.accessibilityStep = value;
}
// accessible = true;
accessible = true;
accessibilityRole = AccessibilityRole.Adjustable;
}