Repository: nvaccess/nvda

Issue:
NVDA doesn't toggle browse mode off when focus is moved to an interactive list
I believe I have discovered a regression in NVDA. @jcsteh can correct me if I am misremembering, but I recall NVDA _used_ to toggle browse mode off if focus was moved to an HTML list made interactive with `tabindex` and keyboard shortcuts implemented in JavaScript. Reason I remember this: while I was working at Twitter I made the timeline an interactive list so that customers could use keyboard shortcuts provided by Twitter to navigate their home timeline. And I recall working with @jcsteh on ensuring this worked in NVDA. Interestingly, the same code example I provided in this bug report works fine in JAWS in that it toggles browse mode off when focus lands on an ARIA `listitem` instance.

### Steps to reproduce:

1. Start NVDA
2. Open https://codepen.io/toddlr/pen/mdBjoJV
3. Use tab-key navigation to move focus to the first ARIA `listitem` in the ARIA `list`

### Actual behavior:
NVDA doesn't toggle browse mode off

### Expected behavior:
NVDA should toggle browse mode off so the up/down arrow keys can be used to navigate (move focus) between listitems in the list

### System configuration
#### NVDA installed/portable/running from source:

#### NVDA version: 2021.3.1

#### Windows version: Windows 10

#### Name and version of other software in use when reproducing the issue:

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your computer?
Yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
No

#### If NVDA add-ons are disabled, is your problem still occurring?
I'm not using any add-ons

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes


Discussion:
I don't think we explicitly intended to switch to focus mode for listitems
with Twitter. If a user wanted to navigate Twitter with Twitter's keyboard
shortcuts, they'd have to force focus mode first anyway, so auto switching
to focus mode wouldn't be necessary. I'm not saying there hasn't been a
change here - there might have been - but even if there was, the behaviour
you desire was not intentional or considered necessary with regard to
Twitter specifically.

While I understand why you don't want to use role="option" here,
role="listitem" (and HTML <li>) is not considered to be a keyboard
navigable widget in the same way that a link is not considered to be a
keyboard navigable widget. Since authors sometimes use focus to force the
user's attention to a specific part of the page, changing this behaviour
isn't without risk.

@jcsteh I know lists are not considered keyboard navigable widgets. That said, I spent the weekend thinking about it and I don't yet have a clear understanding of the practical risks to the user. Especially, if the heuristics used were:

- The list itself has a tabindex (either 0 or -1)
- Each listitem has an `aria-label` or `aria-labelledby`
- The list uses aria-activedescendant or the roving tabindex pattern to manage focus

If those were the signals in HTML and either the list or listitem has focus, what would the practical downside be of toggling browse mode off?
Authors sometimes use tabindex and focus to bring the user's cursor to a
specific (non-keyboard navigable) point in the document. This mostly
happens with SPA documents, but there can be other cases this is done where
for whtaever reason the usual #anchor approach isn't feasible. Normally,
they would target a div or span or some other generic container to achieve
this. I worry there's a very slight chance they could target a <li>
element, since it's arguably just as non-keyboard navigable as a div. With
your proposed change, that would forc efocus mode.

All that said, I think it's very unlikely that an author would do this;
I've certainly never seen it. Given that, it's probably reasonable enough
to implement this. However, I'm no longer a decision makre here, so all of
the above is just informational/opinion on my part. :)

@kloots  Can you please clarify why you can't use role="option" and role="listbox"? I'm sure there is a good reason, but it would be good if it were clearly specified on this issue.
Is it because you know that a screen reader will not render the entire list in browse mode if you do?
I'm not too sure I'm that happy specifically implementing an exception for a single use case here. Also @jcsteh  has pointed out a valid concern, and we don't really know how much that is used in the wild.  
Of course, the screen reader user always has the option of toggling browse mode off, or at least quicknav keys, if they want to use your specific shortcut keys here. 
@michaelDCurran the short answer is `role="option"` will result in each message being rendered as an atomic element and therefore its content will no longer be accessible in browse mode. (It'll have the same negative impact for users of VoiceOver on Mac OS). And browse mode support is important considering how many Slack messages have rich and/or interactive content.

I've taken the time to write up [a doc](https://docs.google.com/document/d/1A0r7YpuO5ppazUd04fgf6l9ItBvHzoxvcubPL6k7Dzw/edit?usp=sharing) detailing all the nuances of this problem for us at Slack including:

- Our design goals/requirements
- The limitations of the existing standards
- The limitations of AT implementations 

I hope it makes clear how nearly every existing option has a downside and how the changes I am proposing in this ticket would help.
I support this proposal. Another example is the downloads manager in MS Edge. Press CTRL+J from any web page, and you land in the exact same pattern. You can then not press Enter on an item until you turn off browse mode. These list items, too, have interactive child elements. JAWS switches to forms mode seamlessly here, making it much more intuitive for beginners and less tech-savvy people. NVDA is unnecessarily complicated and unintuitive here.
Thanks for the vote of confidence @MarcoZehe. Nearly every single list in Slack is a list composed of list items with interactive child elements. So having the screen reader support this pattern would be massively helpful.
> the short answer is `role="option"` will result in each message being rendered as an atomic element and therefore its content will no longer be accessible in browse mode.

Is that actually true, though? Consider this:

`data:text/html,<button>before</button><div role="listbox"><div role="option" tabindex="0"><div role="document" tabindex="0">Some message`

The document is still included in the a11y tree and it's still possible to tab into it. NVDA doesn't switch to browse mode here, which seems like a different bug that would have to be fixed in order for that to work.

The challenge is that you can equally argue that it's not appropriate for listitem to be used for interactive content and that option is more appropriate for that reason. Either way, there are bugs to fix, but as documented in the spec, it's reasonable to argue that option is more appropriate.
FWIW, I'm not saying we shouldn't push for a change to the spec if that proves to be necessary, but it seems to me that there are semantic inconsistencies/problems/bugs either way.
@jcsteh I didn't consider embedding an ARIA document inside an option. I assumed @michaelDCurran was suggesting we just use option. That said, if NVDA isn't switching to browse mode with an ARIA document inside an option it could be the same bug as [12896](https://github.com/nvaccess/nvda/issues/12896). So, no matter what Slack is in a holding pattern here.
@jcsteh and @michaelDCurran I just created [a new example](https://codepen.io/toddlr/pen/JjOryoJ) to examine the possibility of the suggestion of using the ARIA listbox pattern. Unfortunately, this doesn't work at all on the Mac as all children of each ARIA option are presented as text — including the interactive children. Funny enough, I now recall attempting this years ago while I was working at Twitter and gave up on this solution for the same reason.
I would much prefer to go with option/document as @jcsteh  suggests, but also commit to solving #12896.
That really seems the right approach which keeps us as close to standards as possible.
@michaelDCurran going with option/document will not work for us due to lack of compatibility on the Mac ([per this comment](https://github.com/nvaccess/nvda/issues/13221#issuecomment-1039815462)) and changes to VoiceOver requiring an uphill battle as well as upgrades to Mac OS.
Regarding the standards argument: [the heuristics I suggested](https://github.com/nvaccess/nvda/issues/13221#issuecomment-1010280380) are based on standards for keyboard navigation. And per my points in that comment, I'm only suggesting the mode switch happens when DOM focus lands on a listitem made interactive via that pattern. It seemed like a pretty strong signal for switching browse mode off to me.
Hmm. I understand the need for pragmatism, but it feels like a pretty bad
situation to arguably violate the spec because we know something will
likely never get fixed/tweaked in VoiceOver, thus further perpetuating said
spec violation.

That said, I just realised the spec says that role="option" should have
"Children presentational". That is probably why VoiceOver implements it
this way. So we basically can't win either way as far as the spec goes.
Whatever we do, we violate the spec.

Given that, it's probably a lesser violation to go with listitem. Keep in
mind that I don't make the decisions around here, so my opinion is opinion
only. :)

To be honest, I don't understand why what I'm suggesting is being considered a spec violation. Seems more like a gray area to me. And even if it is a spec violation, I also don't yet understand the realistic downsides to what I am suggesting given the specific heuristics I've outlined in [this comment](https://github.com/nvaccess/nvda/issues/13221#issuecomment-1039677241).

Anyway, I've done my very best to outline my reasoning in [the doc](https://docs.google.com/document/d/1A0r7YpuO5ppazUd04fgf6l9ItBvHzoxvcubPL6k7Dzw/edit#heading=h.iemdfq4a4wtk) I wrote. Just today we had another customer complain about the verbosity of the "application" announcements within Slack. Personally, I think the benefits of my suggestion outweigh the spec violation. If NVDA decides to hold the line here, I understand. If that's the case then we'd definitely need [issue 12896](https://github.com/nvaccess/nvda/issues/12896) fixed so we can switch to a single-column ARIA grid as that is our only other practical option. I just don't know if actual users are going to like that.
@kloots  Based on the thorough and healthy discussion here, I am happy enough to go with the listItem approach.
We will switch to focus mode if a listItem gets focus, but only if the list itself is also focusable. I.e. the list has at least tabindex of -1.
thank you for providing the extra information and reasoning here.
@michaelDCurran wow! Thank you so much for hearing me out. This is (hopefully) the end of a very long quest for me that started back when I was at Twitter in 2014.
Just to add to the data:

@jcsteh wrote:

> The document is still included in the a11y tree and it's still possible to tab into it. NVDA doesn't switch to browse mode here, which seems like a different bug that would have to be fixed in order for that to work.

Yeah, but like on the Mac, with JAWS and a Chromium-based browser, the contents in your example is not accessible. So besides VoiceOver on the Mac, there's another hard-to-change contender here where the listbox/option approach simply wouldn't work.
Apologies if I'm hijacking an issue, feel free to tell me that this is 
unrelated and I'll go open another issue.

> Just today we had another customer complain about the verbosity of the 
> "application" announcements within Slack.

I was going to open a ticket about this.  I see application announcements 
in Slack which read link text only when in Browse mode, but which read 
the entire URL *and* the link text when in focus mode.   This is 
extremely tedius.

I don't know if this has anything to do with the issue being discussed, 
but in the case that it does, I add my vote to do whatever is necessary to 
fix it.


Resolving patch:
diff --git a/source/browseMode.py b/source/browseMode.py
--- a/source/browseMode.py
+++ b/source/browseMode.py
@@ -353,10 +353,22 @@ def shouldPassThrough(self, obj, reason: Optional[OutputReason] = None):
 		if not obj.isFocusable and controlTypes.State.FOCUSED not in states and role != controlTypes.Role.POPUPMENU:
 			return False
 		# many controls that are read-only should not switch to passThrough. 
-		# However, certain controls such as combo boxes and readonly edits are read-only but still interactive.
-		# #5118: read-only ARIA grids should also be allowed (focusable table cells, rows and headers).
-		if controlTypes.State.READONLY in states and role not in (controlTypes.Role.EDITABLETEXT, controlTypes.Role.COMBOBOX, controlTypes.Role.TABLEROW, controlTypes.Role.TABLECELL, controlTypes.Role.TABLEROWHEADER, controlTypes.Role.TABLECOLUMNHEADER):
-			return False
+		# However, there are exceptions.
+		if controlTypes.State.READONLY in states:
+			# #13221: For Slack message lists, and the MS Edge downloads window, switch to passthrough
+			# even though the list item and list are read-only, but focusable.
+			if (
+				role == controlTypes.Role.LISTITEM and controlTypes.State.FOCUSED in states
+				and obj.parent.role == controlTypes.Role.LIST and controlTypes.State.FOCUSABLE in obj.parent.states
+			):
+				return True
+			# Certain controls such as combo boxes and readonly edits are read-only but still interactive.
+			# #5118: read-only ARIA grids should also be allowed (focusable table cells, rows and headers).
+			if role not in (
+				controlTypes.Role.EDITABLETEXT, controlTypes.Role.COMBOBOX, controlTypes.Role.TABLEROW,
+				controlTypes.Role.TABLECELL, controlTypes.Role.TABLEROWHEADER, controlTypes.Role.TABLECOLUMNHEADER
+			):
+				return False
 		# Any roles or states for which we always switch to passThrough
 		if role in self.ALWAYS_SWITCH_TO_PASS_THROUGH_ROLES or controlTypes.State.EDITABLE in states:
 			return True


Test patch:
diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py
--- a/tests/system/robot/chromeTests.py
+++ b/tests/system/robot/chromeTests.py
@@ -1652,3 +1652,50 @@ def test_focusTargetReporting():
 		]),
 		message="focus mode - focus with Report Articles enabled"
 	)
+
+
+def test_focus_mode_on_focusable_read_only_lists():
+	"""
+	If a list is read-only, but is focusable, and a list element receives focus, switch to focus mode.
+	"""
+	_chrome.prepareChrome(
+		"""
+		<a href="#">before Target</a>
+		<div role="list" aria-label="Messages" tabindex="-1">
+			<div role="listitem" tabindex="0" aria-label="Todd Kloots Hello all. At 1:30 PM">
+				<div role="document" aria-roledescription="message">
+					<a href="/kloots" class="sender">Todd Kloots</a> <a href="/time" class="time">1:30 PM</a>
+					<p>Hello all.</p>
+				</div>
+			</div>
+		</div>
+		"""
+	)
+	# Set focus
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"before Target",
+			"link",
+		])
+	)
+
+	# focus the list item
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_CALL_SEP.join([
+			SPEECH_SEP.join([
+				"Messages",  # name for list container
+				"list",  # role for list container
+			]),
+			SPEECH_SEP.join([
+				"level 1",  # Inserted by Chromium even though not explicitly set
+				"Todd Kloots Hello all. At 1:30 PM",  # list element name, should read first
+				"1 of 1",  # item count, no role expected here
+			]),
+			"Focus mode",  # Focus mode should be enabled automatically and be indicated
+		]),
+		message="focus mode - focus list item and turn on focus mode"
+	)
diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot
--- a/tests/system/robot/chromeTests.robot
+++ b/tests/system/robot/chromeTests.robot
@@ -120,3 +120,6 @@ Quick Nav reports target first
 Focus reports target first
 	[Documentation]	Focus target should always be reported before ancestors. Ancestors should be reported from inner to outer.
 	test_focusTargetReporting
+focus mode is turned on on focused read-only list item
+	[Documentation]	Focused list items with a focusable list container should cause focus mode to be turned on automatically.
+	test_focus_mode_on_focusable_read_only_lists