Angular.js: Keyboard reordering with ngRepeat drops cursor in Voiceover on OSX

Created on 26 May 2016  路  4Comments  路  Source: angular/angular.js

Do you want to request a _feature_ or report a _bug_?
Bug

What is the current behavior?
When a list rendered with ng-repeat (which uses $animate and jqLite under the hood) is reordered with the keyboard and OSX Voiceover is running, the screen reader cursor becomes detached from the browser's focus state in Safari, Chrome and Firefox. This happens when the underlying data object is manipulated, triggering a re-render; Voiceover can't seem to keep up with the changes, even with focus management (focus sent into the new item). The super buggy part is that it only happens some of the time, as you'll see in the screencast below.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://plnkr.co or similar (template: http://plnkr.co/edit/tpl:yBpEi4).

I recorded a screencast demoing the problem: https://www.youtube.com/watch?v=hqd4ZgQMuK0
Here is the demo code: http://codepen.io/marcysutton/full/RaOPLB

To reproduce:

  1. Open the page in Safari, Chrome or Firefox on a Mac and turn on Voiceover (using command + F5).
  2. Tab onto one of the reorder buttons.
  3. Hit an arrow key to reorder up or down.

What is the expected behavior?

Voiceover should follow the browser's focus point.

What is the motivation / use case for changing the behavior?

ng-repeat is the standard way to render a list to be reordered, yet Voiceover can't keep up. This very well could be a Voiceover platform bug but I wanted to file it here for tracking purposes.

Compare the demo above with another one that does work in Voiceover (but does not use Angular): http://codepen.io/marcysutton/pen/dMEBEr?editors=1010
That demo manipulates the DOM using insertBefore and then updates the data model, instead of the other way around. Voiceover has no problem keeping up with the DOM changes and works with the bare arrow keys (no modifier keys required). Both demos work in JAWS and NVDA on the Windows platform.

Which versions of Angular, and which browser / OS are affected by this issue? Did this work in previous versions of Angular? Please also test with the latest stable and snapshot (https://code.angularjs.org/snapshot/) versions.

Mac OSX 10.11.4 (El Capitan) and Voiceover in Safari, Chrome and Firefox. I can test it with other versions of Angular too, but it happens with 1.5.5.

Other information (e.g. stacktraces, related issues, suggestions how to fix)

If we can't determine a fix here, my plan is to isolate the problem with the underlying raw DOM calls and file the bug with Apple. I spoke with @matsko about it since he knows a lot about $animate, which ng-repeat uses to manipulate the DOM. But my client's giant Angular app needs reordering with ng-repeat, so I'm hoping to find a workaround.

ngRepeat low investigation broken expected use bug

All 4 comments

The fact that it works as expected with JAWS and NVDA is a strong indication that this might indeed be a VoiceOver bug. It is interesting to try to get to the bottom of it and find out what exactly confuses VoiceOver (if that's the case).

(Unfortunately, I don't have access to VoiceOver. I'll give it a go on SauceLabs :smiley:)

It seems to be some bug in how VoiceOver detects (and follows) focus (in combination with how ngRepeat optimizes DOM operations under the hood).

Basically, when you have items A, B and C and click the down arrow on A, the only DOM operation that takes place is parent.insertBefore(B, A). For whatever reason (possibly because of how VoiceOver keeps track of the element it is currently on), VoiceOver places its square on the B button.
Even when you call A-button.focus(), VoiceOver ignores it (and remains on the B button). If I had to guess, I would say this happens, because during the above DOM operations the A element is not touched (and the focus never leaves the A button). As a result, when you call A-button.focus(), VoiceOver does not detect any focus change and (incorrectly) remains on the B button.

As a POC, you can move the focus away of the A button, before calling A-button.focus(), so VoiceOver detects a focus change and places its square on the A button. Unfortunately, there needs to be a delay between moving the focus away and re-focusing A button. Furthermore, simply blurring the button does not work.
Here is a POC pen 1.

In case it's useful, I have created a similar POC exercising the buggy? VoiceOver behavior using vanilla JS: POC pen 2

To sum it up, I believe there are two possible VoiceOver bugs in play here (maybe both):

  1. The way VoiceOver tracks either the item it is on or the position of its square relative to the viewport do not account for changes in the DOM that indirectly affect the currently focused element (or moving a very similarly looking element in the focused element's position).
  2. VoiceOver's way of tracking the focused element and/or calls to .focus() (and/or the dispatching of focus events) may lead to it missing state changes (either due to timing issues or due to incorrect assumptions).

(But I am totally speculating so take everything with two grains of salt.)


With all that said, I believe this is not an Angular issue :grin:

You are awesome. Thank you for exploring this! I will report the bug to Apple and report back.

Since this is not an Angular issue, I'm going to close it.
Thx @marcysutton for the nice report - feel free to post here if you get any updates. (BTW, the VoiceOver bug is tracked here.)

Was this page helpful?
0 / 5 - 0 ratings