Repository: nvaccess/nvda

Issue:
NVDA is not reading checkbox from header table in Firefox browser
<!-- Please read the text in this edit field before filling it in.
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/blob/master/devDocs/githubIssueTemplateExplanationAndExamples.md
Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. 

Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question.
-->

### Steps to reproduce:

1. Open Firefox browser
2. Go to: [https://codepen.io/Maria-Bulboaca/pen/oNMQwow](https://codepen.io/Maria-Bulboaca/pen/oNMQwow)
3. Press tab until you focus the checkbox

### Actual behavior:
<!--
Use "Speak command keys" (NVDA+4) and speech viewer to copy and paste here. 
Use braille viewer to copy and paste here.
You may additionally include an explanation.
-->
When the checkbox is focused the NVDA does not read anything

### Expected behavior:
<!--
Use "Speak command keys" (NVDA+4) and speech viewer to copy and paste here. 
Use braille viewer to copy and paste here.
You may additionally include an explanation.
-->
When the checkbox is focused the NVDA should read 'Select all checkbox not checked'

### NVDA logs, crash dumps and other attachments:
I have attached here an image of the speech viewer when the checkbox is focused.
![image](https://user-images.githubusercontent.com/108864526/216289237-7c5db080-4b22-47a6-aeb5-4679973026f3.png)


### System configuration
#### NVDA installed/portable/running from source:
installed

#### NVDA version:
NVDA 2022.2

#### Windows version:
Windows 10 Enterprise

#### Name and version of other software in use when reproducing the issue:

None
I tested with other screen readers(JAWS 2023 and Narrator) and it works.

### 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.
Yes, NVDA 2022.4 and the NVDA behave the same.

#### If NVDA add-ons are disabled, is your problem still occurring?
Yes

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes


Discussion:

Resolving patch:
diff --git a/source/browseMode.py b/source/browseMode.py
--- a/source/browseMode.py
+++ b/source/browseMode.py
@@ -1406,6 +1406,11 @@ def _set_selection(self, info, reason=OutputReason.CARET):
 				followBrowseModeFocus = config.conf["virtualBuffers"]["autoFocusFocusableElements"]
 				if followBrowseModeFocus or self.passThrough:
 					focusObj.setFocus()
+					# Track this object as NVDA having just requested setting focus to it
+					# So that when NVDA does receive the focus event for it
+					# It can handle it quietly rather than speaking the new focus.
+					if followBrowseModeFocus:
+						self._objPendingFocusBeforeActivate = obj
 			# Queue the reporting of pass through mode so that it will be spoken after the actual content.
 			queueHandler.queueFunction(queueHandler.eventQueue, reportPassThrough, self)
 
@@ -1623,12 +1628,30 @@ def event_gainFocus(self, obj, nextHandler):
 				self._replayFocusEnteredEvents()
 			return nextHandler()
 
-		#We only want to update the caret and speak the field if we're not in the same one as before
-		caretInfo=self.makeTextInfo(textInfos.POSITION_CARET)
-		# Expand to one character, as isOverlapping() doesn't treat, for example, (4,4) and (4,5) as overlapping.
-		caretInfo.expand(textInfos.UNIT_CHARACTER)
-		isOverlapping = focusInfo.isOverlapping(caretInfo)
-		if not self._hadFirstGainFocus or not isOverlapping or (isOverlapping and previousFocusObjIsDefunct):
+		# Save off and clear any previous object that was focused by NVDA
+		# and waiting on a focus event.
+		objPendingFocusBeforeActivate = self._objPendingFocusBeforeActivate
+		self._objPendingFocusBeforeActivate = None
+
+		# We do not want to speak the new focus and update the caret if...
+		if not self._hadFirstGainFocus or previousFocusObjIsDefunct:
+			# still initializing  or the old focus is dead.
+			isOverlapping = False
+		elif config.conf["virtualBuffers"]["autoFocusFocusableElements"]:
+			# if this focus event was caused by NVDA setting the focus itself
+			# Due to auto focus focusable elements option being enabled,
+			# And we detect that the caret was already positioned within the focus.
+			# Note that this is not the default and may be removed in future.
+			caretInfo = self.makeTextInfo(textInfos.POSITION_CARET)
+			# Expand to one character, as isOverlapping() doesn't treat, for example, (4,4) and (4,5) as overlapping.
+			caretInfo.expand(textInfos.UNIT_CHARACTER)
+			isOverlapping = focusInfo.isOverlapping(caretInfo)
+		else:
+			# if this focus event was caused by NVDA setting the focus itself
+			# due to activation or applications key etc.
+			isOverlapping = (obj == objPendingFocusBeforeActivate)
+
+		if not isOverlapping:
 			# The virtual caret is not within the focus node.
 			oldPassThrough=self.passThrough
 			passThrough = self.shouldPassThrough(obj, reason=OutputReason.FOCUS)
@@ -1666,9 +1689,9 @@ def event_gainFocus(self, obj, nextHandler):
 					# Note: this is usually called after the caret movement.
 					vision.handler.handleGainFocus(obj)
 				elif (
-					self._objPendingFocusBeforeActivate
-					and obj == self._objPendingFocusBeforeActivate
-					and obj is not self._objPendingFocusBeforeActivate
+					objPendingFocusBeforeActivate
+					and obj == objPendingFocusBeforeActivate
+					and obj is not objPendingFocusBeforeActivate
 				):
 					# With auto focus focusable elements disabled, when the user activates
 					# an element (e.g. by pressing enter) or presses a key which we pass
@@ -1680,10 +1703,9 @@ def event_gainFocus(self, obj, nextHandler):
 					# the properties before the activation/key, so use that to speak any
 					# changes.
 					speech.speakObject(
-						self._objPendingFocusBeforeActivate,
+						objPendingFocusBeforeActivate,
 						OutputReason.CHANGE
 					)
-					self._objPendingFocusBeforeActivate = None
 			else:
 				self._replayFocusEnteredEvents()
 				return nextHandler()


Test patch: