Repository: nvaccess/nvda

Issue:
NVDA fails to read aria-labeledby label associated with a radio group in browse mode
When using "f" or "r" keys to find radio buttons on a page while in browse mode, NVDA does not read the associated label. At this time I can't say whether this is limited to labels associated by aria-labeledby and other techniques. My current testing example is only with aria-labeledby. My recollection is that all labels are not read. This occurs in all browsers. Tested with IE 11, FF 52, Edge on insider build 15058 and Chrome Canary.

* Actual behavior: the label is only read with focus mode enabled.
* Expected behavior: label should be read the first time the user encounters the radio group similar to what occurs in focus mode.

STR:
1. Open FF.
2. press alt+d to go to the addressbar.
3. Visit the following link: https://ibma.github.io/Va11yS/ARIA/ARIA2_example3.html
4. Navigate to the radio group by repeatedly pressing "f" or pressing "r."




Discussion:
Reading group labels when moving the browse mode cursor would result in duplication, since the group label is usually visible. Of course, you end up skipping the label when you use quick navigation, so we'd have to find some way of differentiating these cases. That said, we get enough complaints as it is that we sometimes read other elements such as landmarks, tables, etc. before the element itself when using quick navigation. I'm concerned we'd end up with similar complaints here. While it obviously makes sense to read the group names when tabbing, using quick navigation normally suggests you know what you're looking for. Why do you feel this is different to landmarks, etc.?
I understand the concern. The difference here is that the label is required for successful form interaction—the  primary function one might say. If the capability of interacting with a radio button in the browse mode is possible, then all necessary information to conduct such interaction must be made available. Otherwise, that interaction is only made half possible.

I take your point, but it's worth noting two things:

1. It is still possible to access the relevant information; you may just need to review a line or two above to get context. That is not unusual when using quick navigation, even for links, list items, etc.
2. There may be other information required for successful interaction; e.g. descriptive text. There are always going to be cases where one needs to review for context when using quick navigation.

See also #3321. That one is related to tabbing, not quick navigation, but they are handled similarly as far as NVDA is concerned. I think we could address both with the same fix, assuming we can figure out what that fix is.

P2 because I guess users might try this without thinking to check context and miss important info, and #3321 is p2 anyway.
good evening.
If I may be allowed a user opinion in this case.
I would vote for when navigating with the arrows by the pages when arriving in a text field or other controls the nvda is spoken the name of the control, without that I have to see the answer.
I as a user without even a programming knowledge I once reported to a website developer that this was a site problem because I thought the site had no description for the object name, in the case a text field.
Other readers pronounce the name of the object by hoaxing it with the arrows, and I got confused a little to understand that nvda does not fix it, and even before reading this call I thought it was a problem in nvda, I discovered that it is a characteristic now.


In the end an account only to substantiate my vote.

Thanks for the wonderful development of nvda, every day that passes I am more enthusiastic about your project.
Update: the URL I referenced in my steps has been updated to https://ibma.github.io/Va11yS/WCAG_ARIA/ARIA2_example3.html

Resolving patch:
diff --git a/source/browseMode.py b/source/browseMode.py
--- a/source/browseMode.py
+++ b/source/browseMode.py
@@ -1284,8 +1284,10 @@ def script_shiftTab(self, gesture):
 	def event_focusEntered(self,obj,nextHandler):
 		if obj==self.rootNVDAObject:
 			self._enteringFromOutside = True
-		if self.passThrough:
-			 nextHandler()
+		# Even if passThrough is enabled, we still completely drop focusEntered events here. 
+		# In order to get them back when passThrough is enabled, we replay them with the _replayFocusEnteredEvents method in event_gainFocus.
+		# The reason for this is to ensure that focusEntered events are delayed until a focus event has had a chance to disable passthrough mode.
+		# As in this case we would  not want them.
 
 	def _shouldIgnoreFocus(self, obj):
 		"""Determines whether focus on a given object should be ignored.
@@ -1317,6 +1319,7 @@ def event_gainFocus(self, obj, nextHandler):
 		self._enteringFromOutside=False
 		if not self.isReady:
 			if self.passThrough:
+				self._replayFocusEnteredEvents()
 				nextHandler()
 			return
 		if enteringFromOutside and not self.passThrough and self._lastFocusObj==obj:
@@ -1327,6 +1330,7 @@ def event_gainFocus(self, obj, nextHandler):
 			return
 		if obj==self.rootNVDAObject:
 			if self.passThrough:
+				self._replayFocusEnteredEvents()
 				return nextHandler()
 			return 
 		if not self.passThrough and self._shouldIgnoreFocus(obj):
@@ -1363,8 +1367,11 @@ def event_gainFocus(self, obj, nextHandler):
 				# However, we still want to update the speech property cache so that property changes will be spoken properly.
 				speech.speakObject(obj,controlTypes.REASON_ONLYCACHE)
 			else:
-				if not oldPassThrough:
-					self._replayFocusEnteredEvents()
+				# Although we are going to speak the object rather than textInfo content, we still need to silently speak the textInfo content so that the textInfo speech cache is updated correctly.
+				# Not doing this would cause  later browseMode speaking to either not speak controlFields it had entered, or speak controlField exits after having already exited.
+				# See #7435 for a discussion on this.
+				speech.speakTextInfo(focusInfo,reason=controlTypes.REASON_ONLYCACHE)
+				self._replayFocusEnteredEvents()
 				nextHandler()
 			focusInfo.collapse()
 			self._set_selection(focusInfo,reason=controlTypes.REASON_FOCUS)
@@ -1375,6 +1382,7 @@ def event_gainFocus(self, obj, nextHandler):
 				# However, we still want to update the speech property cache so that property changes will be spoken properly.
 				speech.speakObject(obj,controlTypes.REASON_ONLYCACHE)
 			else:
+				self._replayFocusEnteredEvents()
 				return nextHandler()
 
 		self._postGainFocus(obj)
diff --git a/source/speech.py b/source/speech.py
--- a/source/speech.py
+++ b/source/speech.py
@@ -726,6 +726,7 @@ def _speakTextInfo_addMath(speechSequence, info, field):
 		return
 
 def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlTypes.REASON_QUERY,index=None,onlyInitialFields=False,suppressBlanks=False):
+	onlyCache=reason==controlTypes.REASON_ONLYCACHE
 	if isinstance(useCache,SpeakTextInfoState):
 		speakTextInfoState=useCache
 	elif useCache:
@@ -851,10 +852,11 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
 		lastLanguage=language
 
 	if onlyInitialFields or (unit in (textInfos.UNIT_CHARACTER,textInfos.UNIT_WORD) and len(textWithFields)>0 and len(textWithFields[0])==1 and all((isinstance(x,textInfos.FieldCommand) and x.command=="controlEnd") for x in itertools.islice(textWithFields,1,None) )): 
-		if onlyInitialFields or any(isinstance(x,basestring) for x in speechSequence):
-			speak(speechSequence)
-		if not onlyInitialFields: 
-			speakSpelling(textWithFields[0],locale=language if autoLanguageSwitching else None)
+		if not onlyCache:
+			if onlyInitialFields or any(isinstance(x,basestring) for x in speechSequence):
+				speak(speechSequence)
+			if not onlyInitialFields: 
+				speakSpelling(textWithFields[0],locale=language if autoLanguageSwitching else None)
 		if useCache:
 			speakTextInfoState.controlFieldStackCache=newControlFieldStack
 			speakTextInfoState.formatFieldAttributesCache=formatFieldAttributesCache
@@ -963,7 +965,7 @@ def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=controlT
 		if not isinstance(useCache,SpeakTextInfoState):
 			speakTextInfoState.updateObj()
 
-	if speechSequence:
+	if not onlyCache and speechSequence:
 		if reason==controlTypes.REASON_SAYALL:
 			speakWithoutPauses(speechSequence)
 		else:
@@ -1160,6 +1162,9 @@ def getControlFieldSpeech(attrs,ancestorAttrs,fieldType,formatConfig=None,extraD
 	elif fieldType=="start_addedToControlFieldStack" and role==controlTypes.ROLE_TABLE and tableID:
 		# Table.
 		return " ".join((nameText,roleText,stateText, getSpeechTextForProperties(_tableID=tableID, rowCount=attrs.get("table-rowcount"), columnCount=attrs.get("table-columncount")),levelText))
+	elif nameText and reason==controlTypes.REASON_FOCUS and fieldType == "start_addedToControlFieldStack" and role==controlTypes.ROLE_GROUPING:
+		# #3321: Report the name of groupings (such as fieldsets) for quicknav and focus jumps
+		return " ".join((nameText,roleText))
 	elif fieldType in ("start_addedToControlFieldStack","start_relative") and role in (controlTypes.ROLE_TABLECELL,controlTypes.ROLE_TABLECOLUMNHEADER,controlTypes.ROLE_TABLEROWHEADER) and tableID:
 		# Table cell.
 		reportTableHeaders = formatConfig["reportTableHeaders"]


Test patch: