X Tutup
The Wayback Machine - https://web.archive.org/web/20260214113443/https://github.com/NativeScript/NativeScript/pull/8909
Skip to content

feat(core): a11y support#8909

Merged
NathanWalker merged 23 commits intoNativeScript:release/8.0.0from
m-abs:feat/accessibility
Jan 29, 2021
Merged

feat(core): a11y support#8909
NathanWalker merged 23 commits intoNativeScript:release/8.0.0from
m-abs:feat/accessibility

Conversation

@m-abs
Copy link
Contributor

@m-abs m-abs commented Sep 27, 2020

PR Checklist

What is the current behavior?

NativeScript doesn't support native accessibility features without our plugin @nota/nativescript-accessibility-ext

Unfortunately that plugin will soon go unmaintained.
I'd hate for the community to loose this feature, so here I'll contribute the feature to upstream NativeScript.

What is the new behavior?

  • Adds new properties to View-class.
  • CSS-helpers.
  • Regain accessibility focus on back navigation.
    etc.

TODO:

Design document here:
https://docs.google.com/document/d/1gybrT1IQh8Fb5r0v_tZTMS06yP6xYmr9pDQIBRbokZc/edit

Implements #6993
Fixes: #6125

BREAKING CHANGES:
Remove the property automationText as it conflicts with the new property accessibilityId the implements the behavior correctly.

[Describe the impact of the changes here.]

Migration steps:
[Provide a migration path for existing applications.]

@cla-bot
Copy link

cla-bot bot commented Sep 27, 2020

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign the CLA at https://www.nativescript.org/cla.
CLA has not been signed by users: @m-abs.
After signing the CLA, you can ask me to recheck this PR by posting @cla-bot check as a comment to the PR.

@m-abs
Copy link
Contributor Author

m-abs commented Sep 27, 2020

@cla-bot check

@cla-bot cla-bot bot added the cla: yes label Sep 27, 2020
@cla-bot
Copy link

cla-bot bot commented Sep 27, 2020

The cla-bot has been summoned, and re-checked this pull request!

@NathanaelA

This comment was marked as abuse.

@m-abs
Copy link
Contributor Author

m-abs commented Sep 28, 2020

Can you create a property automationText that points to automationId so that it is no longer a breaking change?

I can but it won't be exactly the same as the current behavior.

automationId sets automationIndentifier on iOS and doesn't do anything on android.

Currently automationText sets both accessibilityLabel and automationIndentifier on iOS and overrides contentDescription on android.

Hmm... I should probably rename automationId to automationIndentifier, to make it more consistent with the native platform.

@rigor789
Copy link
Member

rigor789 commented Sep 28, 2020

automationId sets automationIndentifier on iOS and doesn't do anything on android.

@m-abs I think for android, we can do something like this:

const id = Utils.android.resources.getId(':id/nativescript_accessibility_id')
this.nativeViewProtected.setTag(id, value)
this.nativeViewProtected.setTag(value)

Tools like wix/Detox use that when getting views by id on android, and accessibilityIdentifier on ios.

I don't 100% recall why I had to set the tag with the id and without, I took that from a sample app I made with detox:
https://github.com/rigor789/ui-tested-nsvue-app/blob/fecd9ed0392b8903d20a0967d87d60201082bc32/app/app.js#L18-L25

In my case I named the property testID as that's the convention in react-native, but whatever is the most consistent should be fine!

@m-abs
Copy link
Contributor Author

m-abs commented Sep 28, 2020

@rigor789

Thanks for working that out. 👍 I'll add it right away.

@farfromrefug
Copy link
Collaborator

A few comments. The accessibility/index.android.ts is not snapshot compatible. It accesses native namespace at its root which will fail in snapshot generation.
I am a bit worried about the whole initA11YView. Storing all views from an app seems "big". Can't we find another way to get the N view from a native view ?
Can we quantify everything which is added to an app not using accessibility?

@NathanaelA

This comment was marked as abuse.

@m-abs
Copy link
Contributor Author

m-abs commented Sep 29, 2020

I am a bit worried about the whole initA11YView. Storing all views from an app seems "big". Can't we find another way to get the N view from a native view ?

We initial made this as a trade off.
In earlier versions of our plugin, we added a NotificationObserver per view, this was as you an imagine very expensive.
The WeakMap from a UIView to a WeakRef was the least expensive and safe way we found to do it. In our experience it doesn't cause too much of a memory overhead.

I've considered binding the View-instance to the native UIView, but I'm not sure if that reference survives when we get it inside the NotificationObserver callback.

@m-abs
Copy link
Contributor Author

m-abs commented Sep 29, 2020

(Responding to this just so others seeing this issue understand, since you already know this) Snapshots are depreciated and unsupported in 7.x; we are not going to continue to mangle the code because of something that is not going to be supported anymore, code cache is the future.

Does this mean, I can ship the ensureNativeClasses-functions? They tend to make the code less readable.

@farfromrefug
Copy link
Collaborator

Please I know you deprecate this but people still use it ! If this PRs gets in as it is it will break snapshots. I would gladly update the code to make it snapshot compatible. It does not represent much work.
And I should point out that the way we write thing for snapshot is actually better performance wise! No no need to Marshal classes if they are not used at runtime !!! (Even If the file is actually imported).
I think N should continue to write android classes that way. Not so much drawback code wise.

As an example here all namespace / classes you added at the top of the file for easy access means they are marshalled (which can be dead slow, look at android R namespace as an example) even though they might never be used.

@NathanaelA I meant I would like to know the consequence of that PR (perfs, men, but mostly perfs) on an app which actually does not "use" or implement accessibility.

@m-abs
Copy link
Contributor Author

m-abs commented Sep 29, 2020

@farfromrefug
Is it better now?

@farfromrefug
Copy link
Collaborator

@m-abs thanks a lot it is perfect now!

@m-abs
Copy link
Contributor Author

m-abs commented Sep 29, 2020

I think, I found a better way to get from the UIView to the nativescript View-instance.

I use the UIView.tag and view._domId.
This adds a overhead for looking up the focused view, but removes the need for the WeakMap.

@NathanaelA

This comment was marked as abuse.

@NathanaelA

This comment was marked as abuse.

Copy link
Contributor

@facetious facetious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few files that are misspelled as acessibility with one c; most notably the folder that they all live within.

* @param {boolean} receivedFocus
* @param {boolean} lostFocus
*/
export function notifyAccessibilityFocusState(view: View, receivedFocus: boolean, lostFocus: boolean): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One boolean would be sufficient.

This method is only used by iOS, but is in a -common. Is that intentional? Apologies; the android file was collapsed by default due to its length.


const oldActiveFontScaleCategory = currentFontScaleCategory;
if (global.isAndroid) {
currentFontScaleCategory = fontExtraMediumClass;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to define these font classes as identical on Android, rather than remove this functionality. It could be useful for developers to be able to target specific font size css classes in their on cascades. Unless I'm misunderstanding the reasoning behind this neutralizing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there is a huge difference between the font-size the user can set on the two platforms, I'll try to explain:

Android:

The font-size is an integer between 0.85 and 1.3 (with some rounding errors).

iOS:

We have the setting preferredContentSizeCategory that maps to a global string constant, they are mapped here:

const sizeMap = new Map<string, number>([
[UIContentSizeCategoryExtraSmall, 0.5],
[UIContentSizeCategorySmall, 0.7],
[UIContentSizeCategoryMedium, 0.85],
[UIContentSizeCategoryLarge, 1],
[UIContentSizeCategoryExtraLarge, 1.15],
[UIContentSizeCategoryExtraExtraLarge, 1.3],
[UIContentSizeCategoryExtraExtraExtraLarge, 1.5],
[UIContentSizeCategoryAccessibilityMedium, 2],
[UIContentSizeCategoryAccessibilityLarge, 2.5],
[UIContentSizeCategoryAccessibilityExtraLarge, 3],
[UIContentSizeCategoryAccessibilityExtraExtraLarge, 3.5],
[UIContentSizeCategoryAccessibilityExtraExtraExtraLarge, 4],
]);

Range 0.5, 0.7, 0.85, 1, 1.15, 1.3, 1.5, 2, 2.5, 3, 3.5, 4

Values <0.85 we call extra small
Values >1.5 we call extra small

The (almost) shared range on iOS and Android 0.85 to 1.5 we call medium size.

We introduced this, because large increases in the font-sizes like 400% breaks most layouts.

I hope that makes sense.


export const AccessibilityServiceEnabledPropName = 'accessibilityServiceEnabled';

export class CommonA11YServiceEnabledObservable extends SharedA11YObservable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed a number of places where you've used A11Y where I would have normally expected A11y. I'm open to discussion here, but my preference is the lowercase y.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering just writing it out as Accessibility :)

[touchExplorationStateEnabledPropName]: boolean;

get accessibilityServiceEnabled(): boolean {
return !!this.accessibilityServiceEnabled && !!this.touchExplorationStateEnabled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like an infinite recursion to me. Did you mean to use accessibilityStateEnabled instead of accessibilityServiceEnabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that is clearly an error.

/**
* Get the Android platform's AccessibilityManager.
*/
export function getAndroidAccessibilityManager(): android.view.accessibility.AccessibilityManager | null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android-specific types should be left out of common .d.ts files. In the remainder of the repo, this is done with : any /* <class name> */.

}

public androidSendAccessibilityEvent(eventName: AndroidAccessibilityEvent, msg?: string): void {
if (!isAccessibilityServiceEnabled()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the value here (and in the following two functions) seems superfluous to me. The final destination of all three ends up within sendAccessibilityEvent, which checks this condition anyway.

/**
* Internal use only. This is used to limit the number of updates to android.view.View.setContentDescription()
*/
_androidContentDescriptionUpdated?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's internal use only, it could be left out of the .d.ts and just locally cast in order for TypeScript to continue validating the type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get a typing error in packages/core/acessibility/index.android.ts if I remove it.

* 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these two methods would be best combined together.

interface AccessibilityEventOptions {
  androidAccessibilityEvent: AndroidAccessibilityEvent;
  iosNotificationType: IOSPostAccessibilityNotificationType;
  message: string;
}

public sendAccessibilityEvent(options: Partial<AccessibilityEventOptions>): void;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

/**
* Announce screen changed
*/
public accessibilityScreenChanged(refocus?: boolean): void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you mean for this to be within the //@endprivate. It should also follow the pattern: onAccessibilityScreenChanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The developer should be able to call this function for large updates on the page.

public enableSwipeBackNavigation: boolean;
public backgroundSpanUnderStatusBar: boolean;
public hasActionBar: boolean;
public accessibilityAnnouncePageEnabled = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property should mirror the definition of the other boolean properties, including specifying the default value elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand.

@farfromrefug

This comment has been minimized.

@NathanWalker NathanWalker added this to the 8.0 milestone Oct 5, 2020
@m-abs
Copy link
Contributor Author

m-abs commented Oct 10, 2020

Hi @NathanaelA

Sorry your reply got lost in all the other replies.

@m-abs - Pardon what might be silly (& stupid) questions, trying to understand the changes as I have no real experience with A11y.

  1. I don't see anything that calls the setContentDescription https://developer.android.com/reference/android/view/View.html#setContentDescription
    I was under the impression that was needed for accessibility support for screen readers.

It is set here: https://github.com/m-abs/NativeScript/blob/feat/accessibility/packages/core/accessibility/index.android.ts#L563-L667

I attempt to recreate the behavior from iOS by concatenating accessibilityLabel, accessibilityValue and accessibilityHint here.

  1. Are you aware that Appium for android uses https://github.com/NativeScript/nativescript-dev-appium (findElementByAccessibilityId) Find by Accessibility Id -- which is the primary method used by most test and why automationText exists in android. This uses the setContentDescription value. It is possible that Igor's fix will fix that??? or perhaps now Appium can actually use FindById via Igor's fix, which has never worked before... Don't know on this, just asking anyone who might now... Before we can merge this; we need to make sure Appium will continue to work and still has a easy to use "byid" element retrieval even if we have to change the internals of our Appium driver ..

We definitely need to have Appium working with this, but my experience with it is very limited.

@rigor789 Do you have some input here? Can FindById find views tagged via setTag?

@NathanWalker NathanWalker changed the base branch from master to release/8.0.0 January 21, 2021 17:33
@NathanWalker NathanWalker changed the title Implements #6993 - Adds upstream accessibility support to nativescript (WIP) feat(core): a11y support Jan 21, 2021
@NathanWalker NathanWalker merged commit bbc8aad into NativeScript:release/8.0.0 Jan 29, 2021
@m-abs m-abs deleted the feat/accessibility branch February 1, 2021 17:34
@vigdora
Copy link

vigdora commented Jun 8, 2021

Hi
@NathanaelA, @rigor789 @m-abs
the change has broken our automation e2e test using Appium,

on android, we used to benefit from using automationText prop
since it added the necessary content-desc attribute to android's elements.
this way Appium managed to find those elements using the accessibility id selector.

for reference - look at the NS7 code, where setContentDescription is used:
image

and in NS8 it looks now like this: (probably for solving the issue for Detox platform):
@nativescript/core/ui/core/view/index.android.js
image

please notice that in NS8, automationText now points to accessibilityIdentifierProperty:
image

to summerise:
as you can see, setContentDescription was removed, thus all our automation tests on android are broken.
can the previous (NS7) solution can be combined to the new one?

@m-abs
Copy link
Contributor Author

m-abs commented Jun 8, 2021

hi @vigdora,

I'm sorry this causes you problems.

can the previous (NS7) solution can be combined to the new one?

No, that would make the app inaccessible for accessibility users on android and defeat the purpose of this PR.

The old implementation was wrong, Android's setContentDescription" is not equivalent of iOS's accessibilityIdentifier`, they serve very different purposes.

Unfortunately you have to update your tests to make them work with this change.

I haven't worked with appium myself, but maybe this can help you:
https://github.com/rigor789/ui-tested-nsvue-app/blob/fecd9ed0392b8903d20a0967d87d60201082bc32/e2e/firstTest.spec.js#L24

Using setTag was suggested by @rigor789 who linked to that repo.

@vigdora
Copy link

vigdora commented Jun 9, 2021

@m-abs
this is tough...
as far as I know, Appium doesn't let us find elements easily cross app except using the Accessibility ID selector.
without this selector, I will need to use xpath or other strategies which will make testing way inconvenient.

The link you referred to doesn't help much since it uses Detox or still uses the Accessibility ID with Appium.

@farfromrefug
Copy link
Collaborator

@vigdora there is a quick workaround. Create a plugin which adds a mixin N. In that mixin override the [accessibilityIdentifierProperty.setNative] function. In there call setContentDescription as well as the original method.
I could even be part of the nativescript appium plugin.

@vigdora
Copy link

vigdora commented Jun 9, 2021

@farfromrefug
I already tried it - but it doesnt work since the function applyContentDescription (found in @nativescript/core/accessibility/index.android.js)
is clearing the contentdescription if no accessibility attribute (i.e. accessibilityLabel) is presented on the element:

image

@m-abs
may be we can remove the contentdescription clearance as depicted in the snapshot I shared (red arrow).
than I can apply @farfromrefug workaround

@m-abs
Copy link
Contributor Author

m-abs commented Jun 9, 2021

@farfromrefug and @vigdora
I don't think this discussion belongs here on a merged PR.

You should probably create a new issue about it and maybe take the discussion on slack.

@farfromrefug
Copy link
Collaborator

@m-abs thanks we all take it along from here.
@vigdora I think we might be able to remove it. could you try ?

@vigdora
Copy link

vigdora commented Jun 13, 2021

I gave it another thought and removing androidView.setContentDescription(null) as I mentioned in my previous comment still wont help as text attribute (textValue) will populate content-description on android any way.
image

so I honestly have no quick solution for our automation tests,
I would have liked to open a PR for fixing it but the only solution I see right now is to allow disabling the accessibility feature on android once a developer choses to use the automationText attribute.
what are the chances for it to be approved?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature(iOS): Fonts are not scaled for a11y in nativescript.

7 participants

X Tutup